{"id":1364,"date":"2026-03-28T10:58:24","date_gmt":"2026-03-28T09:58:24","guid":{"rendered":"https:\/\/jankiewicz.pl\/?p=1364"},"modified":"2026-03-28T12:17:03","modified_gmt":"2026-03-28T11:17:03","slug":"czas-w-esperze-kiedy-warto-wziac-go-we-wlasne-rece","status":"publish","type":"post","link":"https:\/\/jankiewicz.pl\/index.php\/czas-w-esperze-kiedy-warto-wziac-go-we-wlasne-rece\/","title":{"rendered":"Czas w Esperze: kiedy warto wzi\u0105\u0107 go we w\u0142asne r\u0119ce"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Kiedy czas systemowy wystarcza<\/h2>\n\n\n\n<p>Esper domy\u015blnie opiera si\u0119 na zegarze systemowym. Dla aplikacji przetwarzaj\u0105cych zdarzenia na bie\u017c\u0105co \u2013 strumie\u0144 transakcji, dane IoT, logi w czasie rzeczywistym \u2013 to podej\u015bcie jest naturalne i w pe\u0142ni wystarczaj\u0105ce. Problem pojawia si\u0119 wtedy, gdy chcemy przetwarza\u0107 <strong>dane historyczne<\/strong> lub <strong>testowa\u0107 zapytania na danych spreparowanych<\/strong>. Wtedy zegar systemowy przestaje nam pomaga\u0107, a zaczyna przeszkadza\u0107.<\/p>\n\n\n\n<p>Je\u015bli przetwarzamy dane historyczne ale nasze zapytania EPL bazuj\u0105 na <code>#ext_timed<\/code> lub <code>#ext_timed_batch<\/code>, to Esper wyznacza granice okien na podstawie znacznika czasowego wbudowanego w samo zdarzenie \u2013 i zegar systemowy w og\u00f3le nie wchodzi tu w gr\u0119. Mo\u017cemy spokojnie pisa\u0107:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">select spolka, avg(kursOtwarcia)\nfrom KursAkcji#ext_timed(data.getTime(), 7 days)\ngroup by spolka<\/pre>\n\n\n\n<p>Esper nie patrzy wtedy na zegarek \u2013 patrzy na warto\u015b\u0107 <code>data<\/code> w ka\u017cdym zdarzeniu. To eleganckie i niezawodne rozwi\u0105zanie dla okien opartych na atrybucie zdarzenia.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Kiedy czas systemowy zaczyna przeszkadza\u0107<\/h2>\n\n\n\n<p>Nie wszystkie konstrukcje EPL maj\u0105 sw\u00f3j odpowiednik <code>ext_timed<\/code>. Wzorce (<code>pattern<\/code>) korzystaj\u0105ce z <code>timer:interval<\/code>, <code>timer:within<\/code> czy <code>timer:at<\/code>, a tak\u017ce zwyk\u0142e okna <code>#time<\/code> oraz klauzula <code>output<\/code> (<code>output every 3 seconds<\/code>, <code>output every 1 minutes<\/code> itp.) \u2013 wszystkie te elementy opieraj\u0105 si\u0119 wy\u0142\u0105cznie na <strong>wewn\u0119trznym zegarze Espera<\/strong>. Nie istnieje dla nich wersja &#8222;ext&#8221;, kt\u00f3ra pozwoli\u0142aby poda\u0107 w\u0142asny znacznik czasowy.<\/p>\n\n\n\n<p>Wyobra\u017amy sobie zapytanie wykrywaj\u0105ce sytuacj\u0119, w kt\u00f3rej po kursie powy\u017cej 100 nie pojawi\u0142 si\u0119 \u017caden kurs powy\u017cej 95 przez wi\u0119cej ni\u017c 3 dni:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@name('alert')\npattern [\n    every a=KursAkcji(kursOtwarcia > 100)\n    -> (timer:interval(3 days) and not KursAkcji(kursOtwarcia > 95))\n]<\/pre>\n\n\n\n<p><code>timer:interval(3 days)<\/code> pyta wprost wewn\u0119trzny zegar Espera: &#8222;czy min\u0119\u0142y 3 dni?&#8221;. Gdy uruchomimy to zapytanie na danych historycznych z 2001 roku, a zegar systemowy pokazuje 2025 \u2013 wzorzec nigdy nie wyzwoli si\u0119 poprawnie. Esper siedzi w roku 2025 i czeka na 3 doby liczone od teraz, a nie od znacznika w danych.<\/p>\n\n\n\n<p>Ten sam problem dotyczy okien <code>#time<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">select spolka, avg(kursOtwarcia)\nfrom KursAkcji#time(3 days)\ngroup by spolka<\/pre>\n\n\n\n<p>Przy danych historycznych zdarzenia trafiaj\u0105 do okna, ale bardzo d\u0142ugo z niego nie wypadaj\u0105 \u2013 bo zegar systemowy idzie do przodu w tempie rzeczywistym, nie w tempie danych.<\/p>\n\n\n\n<p>To samo dotyczy klauzuli <code>output<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"sql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">select spolka, avg(kursOtwarcia)\nfrom KursAkcji#time(7 days)\ngroup by spolka\noutput every 1 days<\/pre>\n\n\n\n<p>W powy\u017cszym przyk\u0142adzie musieliby\u015bmy czeka\u0107 ca\u0142y dzie\u0144 na pierwszy z wynik\u00f3w.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Dane historyczne i \u015brodowisko testowe<\/h2>\n\n\n\n<p>Drugi wa\u017cny przypadek to <strong>testowanie<\/strong>. Nawet je\u015bli docelowo aplikacja b\u0119dzie dzia\u0142a\u0107 w czasie rzeczywistym, podczas tworzenia i weryfikowania zapyta\u0144 operujemy na zbiorach przygotowanych danych. Chcemy sprawdzi\u0107, czy okno 7-dniowe poprawnie wygasa, czy wzorzec czasowy wyzwala si\u0119 we w\u0142a\u015bciwym momencie \u2013 i chcemy to sprawdzi\u0107 <strong>teraz<\/strong>, nie czekaj\u0105c 7 prawdziwych dni.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rozwi\u0105zanie: wy\u0142\u0105cz timer i we\u017a czas we w\u0142asne r\u0119ce<\/h2>\n\n\n\n<p>Esper oferuje do tego dedykowany mechanizm. Wystarcz\u0105 dwie rzeczy.<\/p>\n\n\n\n<p><strong>1. Wy\u0142\u0105czenie wewn\u0119trznego timera przy konfiguracji:<\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Configuration config = new Configuration();\nconfig.getRuntime().getThreading().setInternalTimerEnabled(false);<\/pre>\n\n\n\n<p>Od tej chwili Esper nie pyta systemu operacyjnego o czas. Jego wewn\u0119trzny zegar stoi w miejscu dop\u00f3ki sami go nie przesuniemy.<\/p>\n\n\n\n<p><strong>2. Ustawianie czasu przed wysy\u0142k\u0105 zdarze\u0144:<\/strong><\/p>\n\n\n\n<p>Pole <code>data<\/code> obecne w ka\u017cdym zdarzeniu niesie informacj\u0119 o tym, w kt\u00f3rym momencie logicznym zdarzenie powinno by\u0107 przetworzone. Przed wysy\u0142k\u0105 ka\u017cdego zdarzenia odczytujemy t\u0119 warto\u015b\u0107 i przesuwamy zegar Espera \u2013 ale tylko wtedy, gdy <code>data<\/code> si\u0119 zmieni\u0142a wzgl\u0119dem poprzedniego zdarzenia (zdarzenia z tej samej sesji gie\u0142dowej maj\u0105 identyczn\u0105 dat\u0119 i s\u0105 dla Espera r\u00f3wnoczesne z punktu widzenia czasu):<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">long previousTime = Long.MIN_VALUE;\nfor (String json : events) {\n    String dataStr = Json.parse(json).asObject().getString(\"data\", null);\n    long eventTime = Timestamp.valueOf(dataStr).getTime();\n    if (eventTime != previousTime) {\n        runtime.getEventService().advanceTime(eventTime);\n        previousTime = eventTime;\n    }\n    runtime.getEventService().sendEventJson(json, \"KursAkcji\");\n}<\/pre>\n\n\n\n<p>Ca\u0142e tygodnie danych historycznych mo\u017cemy &#8222;przewin\u0105\u0107&#8221; w u\u0142amku sekundy, a <code>timer:interval(3 days)<\/code> wyzwoli si\u0119 dok\u0142adnie wtedy, gdy wynika to z pola <code>data<\/code>, a nie z zegara \u015bciennego.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Okna oparte na liczbie zdarze\u0144 (<code>#length<\/code>, <code>#length_batch<\/code>) dzia\u0142aj\u0105 niezale\u017cnie od zegara i przetwarzaj\u0105 ka\u017cde zdarzenie indywidualnie.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">Jak to wygl\u0105da w praktyce<\/h2>\n\n\n\n<p>Poni\u017cszy kompletny przyk\u0142ad pokazuje zapytanie z oknem <code>#time(3 days)<\/code> na danych historycznych z 2001 roku. Bez sterowania czasem zdarzenia trafia\u0142yby do okna, ale nie wypada\u0142y z niego przez zegarowe 3 dni (dekady w czasie z naszych danych) \u2013 bo zegar systemowy idzie do przodu w tempie rzeczywistym. Dzi\u0119ki <code>advanceTime<\/code> Esper \u017cyje w roku 2001 i okno dzia\u0142a dok\u0142adnie tak jak powinno.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Configuration config = new Configuration();\nconfig.getRuntime().getThreading().setInternalTimerEnabled(false);\n\nEPCompiler compiler = EPCompilerProvider.getCompiler();\nEPCompiled compiled = compiler.compile(\"\"\"\n        @public @buseventtype\n        create json schema KursAkcji(spolka string, kursOtwarcia double, data string);\n\n        @name('avg3days')\n        select spolka, avg(kursOtwarcia) as srednia, count(*) as liczba\n        from KursAkcji#time(3 days)\n        group by spolka\n        output snapshot every 1 events;\n        \"\"\", new CompilerArguments(config));\n\nEPRuntime runtime = EPRuntimeProvider.getRuntime(\"demo\", config);\nEPDeployment deployment = runtime.getDeploymentService().deploy(compiled);\n\nruntime.getDeploymentService()\n        .getStatement(deployment.getDeploymentId(), \"avg3days\")\n        .addListener((newData, oldData, stmt, rt) -> {\n            for (EventBean eb : newData) {\n                System.out.printf(\"%-8s  srednia=%.2f  w oknie=%s zdarzen%n\",\n                        eb.get(\"spolka\"),\n                        ((Number) eb.get(\"srednia\")).doubleValue(),\n                        eb.get(\"liczba\"));\n            }\n        });\n\nString[] data = {\n    \/\/ Sesja 2001-09-04\n    \"{\\\"spolka\\\":\\\"Apple\\\",\\\"kursOtwarcia\\\":18.50,\\\"data\\\":\\\"2001-09-04 00:00:00.0\\\"}\",\n    \"{\\\"spolka\\\":\\\"IBM\\\",  \\\"kursOtwarcia\\\":100.15,\\\"data\\\":\\\"2001-09-04 00:00:00.0\\\"}\",\n    \/\/ Sesja 2001-09-05  (+1 dzie\u0144)\n    \"{\\\"spolka\\\":\\\"Apple\\\",\\\"kursOtwarcia\\\":18.24,\\\"data\\\":\\\"2001-09-05 00:00:00.0\\\"}\",\n    \"{\\\"spolka\\\":\\\"IBM\\\",  \\\"kursOtwarcia\\\":101.50,\\\"data\\\":\\\"2001-09-05 00:00:00.0\\\"}\",\n    \/\/ Sesja 2001-09-07  (+3 dni od sesji pierwszej -> sesja 04.09 wypada z okna)\n    \"{\\\"spolka\\\":\\\"Apple\\\",\\\"kursOtwarcia\\\":17.50,\\\"data\\\":\\\"2001-09-07 00:00:00.0\\\"}\",\n    \"{\\\"spolka\\\":\\\"IBM\\\",  \\\"kursOtwarcia\\\":97.90, \\\"data\\\":\\\"2001-09-07 00:00:00.0\\\"}\",\n};\n\nlong previousTime = Long.MIN_VALUE;\nfor (String json : data) {\n    String dataStr = Json.parse(json).asObject().getString(\"data\", null);\n    long eventTime = Timestamp.valueOf(dataStr).getTime();\n    if (eventTime != previousTime) {\n        runtime.getEventService().advanceTime(eventTime);\n        System.out.println(\"\\n>>> \" + dataStr);\n        previousTime = eventTime;\n    }\n    runtime.getEventService().sendEventJson(json, \"KursAkcji\");\n}\n\nruntime.destroy();<\/pre>\n\n\n\n<p>Oczekiwany wynik pokazuje, \u017ce sesja z 4 wrze\u015bnia wypada z okna dok\u0142adnie wtedy, gdy <code>advanceTime<\/code> przesunie zegar do 7 wrze\u015bnia \u2013 bo min\u0119\u0142y w\u0142a\u015bnie 3 doby liczone w czasie danych, nie w czasie rzeczywistym:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&gt;&gt;&gt; 2001-09-04 00:00:00.0\nApple     srednia=18.50  w oknie=1 zdarzen\nIBM       srednia=100.15 w oknie=1 zdarzen\n\n&gt;&gt;&gt; 2001-09-05 00:00:00.0\nApple     srednia=18.37  w oknie=2 zdarzen\nIBM       srednia=100.83 w oknie=2 zdarzen\n\n&gt;&gt;&gt; 2001-09-07 00:00:00.0\nApple     srednia=17.87  w oknie=2 zdarzen   \u2190 sesja 04.09 wypad\u0142a z okna\nIBM       srednia=99.70  w oknie=2 zdarzen<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Podsumowanie<\/h2>\n\n\n\n<p>Czas systemowy w Esperze jest wygodny i wystarczaj\u0105cy dla aplikacji przetwarzaj\u0105cych zdarzenia na \u017cywo, gdy okna opieraj\u0105 si\u0119 na <code>#ext_timed<\/code>. Gdy jednak wchodzimy w obszar danych historycznych, testowania na danych spreparowanych lub u\u017cywamy wzorc\u00f3w z <code>timer:interval<\/code> i <code>timer:within<\/code> \u2013 zegar systemowy staje si\u0119 przeszkod\u0105. <code>advanceTime<\/code> sterowany polem <code>data<\/code> ze zdarzenia to prosty i elegancki spos\u00f3b, by odda\u0107 kontrol\u0119 nad czasem w r\u0119ce danych.<\/p>\n\n\n\n<p><em>Tekst powsta\u0142 przy wsp\u00f3\u0142pracy z Claude (Anthropic). Tre\u015b\u0107 zosta\u0142a poprawiona, zredagowana i zweryfikowana merytorycznie przez autora.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Kiedy czas systemowy wystarcza Esper domy\u015blnie opiera si\u0119 na zegarze systemowym. Dla aplikacji przetwarzaj\u0105cych zdarzenia na bie\u017c\u0105co \u2013 strumie\u0144 transakcji, dane IoT, logi w czasie&hellip;<\/p>\n","protected":false},"author":2,"featured_media":1368,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[55],"tags":[],"class_list":["post-1364","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-stuku-puku"],"_links":{"self":[{"href":"https:\/\/jankiewicz.pl\/index.php\/wp-json\/wp\/v2\/posts\/1364","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jankiewicz.pl\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/jankiewicz.pl\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/jankiewicz.pl\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/jankiewicz.pl\/index.php\/wp-json\/wp\/v2\/comments?post=1364"}],"version-history":[{"count":8,"href":"https:\/\/jankiewicz.pl\/index.php\/wp-json\/wp\/v2\/posts\/1364\/revisions"}],"predecessor-version":[{"id":1375,"href":"https:\/\/jankiewicz.pl\/index.php\/wp-json\/wp\/v2\/posts\/1364\/revisions\/1375"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/jankiewicz.pl\/index.php\/wp-json\/wp\/v2\/media\/1368"}],"wp:attachment":[{"href":"https:\/\/jankiewicz.pl\/index.php\/wp-json\/wp\/v2\/media?parent=1364"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/jankiewicz.pl\/index.php\/wp-json\/wp\/v2\/categories?post=1364"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/jankiewicz.pl\/index.php\/wp-json\/wp\/v2\/tags?post=1364"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}