diff options
| -rw-r--r-- | Makefile | 6 | ||||
| -rw-r--r-- | serve/serve.c | 23 | ||||
| -rw-r--r-- | tests/check.py | 35 | ||||
| -rw-r--r-- | tests/clock-back.c | 23 |
4 files changed, 74 insertions, 13 deletions
@@ -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; +} |
