aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-29 18:04:24 +0100
committereunhhu <[email protected]>2026-09-29 18:04:43 +0100
commit8fe9ce66573be53ff56fe2a8afc693dabeae9585 (patch)
treeb556788225296e84a6a5082c2927295588a6955a
parente710b93588782135365f01bb8351edb3bf4f95ac (diff)
downloadpure-8fe9ce66573be53ff56fe2a8afc693dabeae9585.tar.gz
pure-8fe9ce66573be53ff56fe2a8afc693dabeae9585.zip
Tick a stateless app's timers, and wait for them on the monotonic clock
run_timers ticked nothing in an app without states, taking a timer to be able to change nothing else; but a tick may send, and the outbox is the app's state (the host gives such an app an owner for it). A timer that only sends never ticked (review 004, F4). Now an app ticks when it has states or outputs; one with neither runs on every connection thread and has nothing a tick could do. When a timer is due was reckoned on the wall clock: set back, a timer waited for the time it had reached again (review 004, an earlier P2). The next tick is now reckoned on the monotonic clock; events still see the wall clock (now). check.py's server_timers runs the review's heartbeat (a timer, an output, no state) and sees it tick, then again with the wall clock set back an hour 0.2 s in (build/clock-back.so, an LD_PRELOAD of clock_gettime): 29 ticks in 0.6 s, where the server before this stops at 9.
-rw-r--r--Makefile6
-rw-r--r--serve/serve.c23
-rw-r--r--tests/check.py35
-rw-r--r--tests/clock-back.c23
4 files changed, 74 insertions, 13 deletions
diff --git a/Makefile b/Makefile
index d14510e..3e3e143 100644
--- a/Makefile
+++ b/Makefile
@@ -85,6 +85,10 @@ $(BUILD)/test-message-json: serve/test-message-json.c serve/message-json.c $(BUI
$(CC) $(CPPFLAGS) $(CFLAGS) $(INCLUDES) -Iserve serve/test-message-json.c serve/message-json.c $(BUILD)/libpure.a \
$(LDFLAGS) -lm $(LDLIBS) -o $@
+# LD_PRELOAD: the wall clock set back an hour, to see timers go on (tests/check.py).
+$(BUILD)/clock-back.so: tests/clock-back.c
+ $(CC) $(CPPFLAGS) $(CFLAGS) -fPIC -shared tests/clock-back.c $(LDFLAGS) -ldl $(LDLIBS) -o $@
+
# A module run ranged and not from the same inputs, compared (tests/check.py).
$(BUILD)/module-probe: tests/module-probe.c $(BUILD)/libpure.a $(HEADERS)
$(CC) $(CPPFLAGS) $(CFLAGS) $(INCLUDES) tests/module-probe.c $(BUILD)/libpure.a $(LDFLAGS) -lm $(LDLIBS) -o $@
@@ -101,7 +105,7 @@ $(BUILD)/test-tree-store: store/test-tree-store.c $(STORE) $(HEADERS)
# compare documents with the earlier document server.
AUP ?= ../aup
check: all $(BUILD)/test-module $(BUILD)/test-tree-store $(BUILD)/test-log $(BUILD)/test-link $(BUILD)/test-message-json \
- $(BUILD)/module-probe
+ $(BUILD)/module-probe $(BUILD)/clock-back.so
$(BUILD)/test-tree-store
$(BUILD)/test-log
$(BUILD)/test-module
diff --git a/serve/serve.c b/serve/serve.c
index b271d14..96e1b1d 100644
--- a/serve/serve.c
+++ b/serve/serve.c
@@ -183,7 +183,7 @@ typedef struct {
bool wrote; /* the last round saved an event or an edit */
uint64_t save_waited; /* ns the last save waited for the device (a sync), not working */
bool reads_first; /* the owner's: a round's reads before its writes */
- double next[PURE_MODULE_INPUTS]; /* when each timer ticks next (wall clock, ms) */
+ uint64_t next[PURE_MODULE_INPUTS]; /* when each timer ticks next (monotonic clock, ms; 0: not yet begun) */
uint64_t ticks;
/* The owner's: the last save's snapshot (NULL: the owner shows views),
* taken under the lock, and the app's changes it shows. */
@@ -1648,27 +1648,32 @@ static void execute(Executor *ex, const Job *jobs, size_t count, void (*finish)(
if (pinned) pure_store_pin(ex->store, false);
}
-/* Tick every timer that is due, with one save for them (an app with states
- * only: a timer can change nothing else). The milliseconds until the next
- * is due, or -1 without timers. Missed ticks are not made up. */
+/* Tick every timer that is due, with one save for them. A tick can change
+ * states and send (the outbox is the app's state too): an app with neither
+ * has nothing a tick could do, and runs on every thread, so it ticks none.
+ * The milliseconds until the next is due, or -1 without timers. Missed
+ * ticks are not made up. When a timer is due is the monotonic clock's
+ * business (the wall clock may be set back); events see the wall clock. */
static long run_timers(Executor *ex) {
const PureModule *m = ex->module;
- if (!m->states) return -1;
- double now = wall_ms(), soonest = -1;
+ bool changes = m->states > 0;
+ for (uint32_t i = 0; i < m->inputs && !changes; ++i) changes = m->input[i].kind == PURE_KIND_OUTPUT;
+ if (!changes) return -1;
+ uint64_t now = now_ms(), soonest = UINT64_MAX;
bool ticked = false;
for (uint32_t i = 0; i < m->inputs; ++i) {
if (m->input[i].kind != PURE_KIND_TIMER) continue;
if (!ex->next[i]) ex->next[i] = now + m->input[i].period; /* the first period starts now */
if (ex->next[i] <= now) {
- ex->app.now = now;
+ ex->app.now = wall_ms();
if (pure_app_tick(&ex->app, i) == PURE_OK) ++ex->ticks;
ticked = true;
ex->next[i] = now + m->input[i].period;
}
- if (soonest < 0 || ex->next[i] - now < soonest) soonest = ex->next[i] - now;
+ if (ex->next[i] - now < soonest) soonest = ex->next[i] - now;
}
if (ticked && pure_app_save(&ex->app) == PURE_OK) after_save(ex); /* a failed save undoes the ticks */
- return soonest < 0 ? -1 : (long)ceil(soonest);
+ return soonest == UINT64_MAX ? -1 : (long)soonest;
}
/* On the request's connection thread: complete its ticket and answer it if
diff --git a/tests/check.py b/tests/check.py
index cbcf6cb..de4f066 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -55,10 +55,10 @@ def compile_module(aup, source_path, out):
class HttpHost:
"""A Pure HTTP host on a fresh port; stops with its statistics."""
- def __init__(self, args):
+ def __init__(self, args, env=None):
import select
self.process = subprocess.Popen([str(a) for a in args] + ['--port', '0'], stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
+ stderr=subprocess.PIPE, env=env)
ready, _, _ = select.select([self.process.stdout], [], [], 10)
assert ready, 'HTTP host did not start'
line = self.process.stdout.readline()
@@ -2329,10 +2329,18 @@ view stamped_view = stamped
"""
+HEARTBEAT_SOURCE = """input beat = every 20 ms
+output pulse = message { value: number }
+on beat { send pulse { value: 1 } }
+view label = "heartbeat"
+"""
+
+
def server_timers(aup, pure, work):
"""A served app's timers tick on the server's clock and its events see
the time they were applied, on one thread and with an owner; the ticks
- are saved, so a restarted server goes on from them."""
+ are saved, so a restarted server goes on from them. An app with no state
+ but an output ticks too: its sends are its state (review 004, F4)."""
import time
module, source = work / 'beat.pmod', work / 'beat.aup'
source.write_text(BEAT_SOURCE)
@@ -2358,6 +2366,27 @@ def server_timers(aup, pure, work):
stats = host.stop()
assert stats[0]['ticks'] >= 5, stats
seen[threads] = beats
+ module, source = work / 'heartbeat.pmod', work / 'heartbeat.aup'
+ source.write_text(HEARTBEAT_SOURCE)
+ compile_module(aup, source, module)
+ host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'heartbeat'])
+ try:
+ time.sleep(0.45)
+ finally:
+ stats = host.stop()
+ assert stats[0]['ticks'] >= 10, stats
+ seen['stateless'] = stats[0]['ticks']
+ # The wall clock set back an hour 0.2 s in (build/clock-back.so): the
+ # timers wait on the monotonic clock, so they go on ticking.
+ import os
+ host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'heartbeat-back'],
+ env={**os.environ, 'LD_PRELOAD': str(pure / 'build/clock-back.so')})
+ try:
+ time.sleep(0.6)
+ finally:
+ stats = host.stop()
+ assert stats[0]['ticks'] >= 20, stats
+ seen['clock_set_back'] = stats[0]['ticks']
return seen
diff --git a/tests/clock-back.c b/tests/clock-back.c
new file mode 100644
index 0000000..50870ca
--- /dev/null
+++ b/tests/clock-back.c
@@ -0,0 +1,23 @@
+#define _GNU_SOURCE
+/* LD_PRELOAD: the wall clock (CLOCK_REALTIME) set back an hour from
+ * CLOCK_BACK_MS milliseconds (200 unless given) after the process first
+ * asks for it, as a person or NTP may set it. A program's timers must go on
+ * all the same: they wait on the monotonic clock (tests/check.py). */
+#include <dlfcn.h>
+#include <stdlib.h>
+#include <time.h>
+
+int clock_gettime(clockid_t id, struct timespec *t) {
+ static int (*real)(clockid_t, struct timespec *);
+ static long long began = -1;
+ if (!real) *(void **)&real = dlsym(RTLD_NEXT, "clock_gettime"); /* as POSIX's dlsym example does */
+ int result = real(id, t);
+ if (result || id != CLOCK_REALTIME) return result;
+ struct timespec now;
+ real(CLOCK_MONOTONIC, &now);
+ long long ms = (long long)now.tv_sec * 1000 + now.tv_nsec / 1000000;
+ if (began < 0) began = ms;
+ const char *after = getenv("CLOCK_BACK_MS");
+ if (ms - began >= (after ? atoll(after) : 200)) t->tv_sec -= 3600;
+ return result;
+}