aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--serve/serve.c132
-rw-r--r--tests/check.py40
2 files changed, 126 insertions, 46 deletions
diff --git a/serve/serve.c b/serve/serve.c
index e53d023..fc1a063 100644
--- a/serve/serve.c
+++ b/serve/serve.c
@@ -215,9 +215,10 @@ typedef struct {
uint64_t memory_refused; /* events refused for the app's memory budget */
bool answers_wait; /* an answer the app had no room for: tried again after ANSWER_WAIT_MS */
/* The owner's: where its snapshots go (NULL: it shows its views
- * itself), and the app's changes the last one shows. */
+ * itself), and the app's changes and module the last one shows. */
Published *published;
uint64_t published_changes, publications;
+ const PureModule *published_module;
struct PureSurfaceHost *surface; /* told of each snapshot published */
/* The owner's: outputs wired to HTTP receivers, and how far each was trimmed. */
PureWire *wires[PURE_MODULE_INPUTS];
@@ -1679,7 +1680,8 @@ static void surface_changed(PureSurfaceHost *s) {
* saved, so a read made after a write's answer sees the write. A failed save
* returns the app to the last save's states, which the snapshot still shows. */
static void publish(Executor *ex) {
- if (!ex->published || (ex->publications && ex->published_changes == ex->app.changes)) return;
+ if (!ex->published || (ex->publications && ex->published_changes == ex->app.changes && ex->published_module == ex->module))
+ return;
PureSnapshot *made = pure_app_snapshot(&ex->app), *old;
pthread_mutex_lock(&ex->published->lock);
old = ex->published->snapshot;
@@ -1687,6 +1689,7 @@ static void publish(Executor *ex) {
pthread_mutex_unlock(&ex->published->lock);
pure_snapshot_release(old);
ex->published_changes = ex->app.changes;
+ ex->published_module = ex->module;
++ex->publications;
if (ex->surface) surface_changed(ex->surface);
}
@@ -3673,24 +3676,14 @@ static bool more_workers(void) {
return true;
}
-/* An app of a host of apps starts in its place: its store (none another
- * running app has), its owner on it, its outlets and its snapshot; then it
- * takes turns and requests, and the link streams to its inputs find them
- * again. False, why in app->why, when it cannot: it stays stopped. */
-static bool start_app(App *app) {
- struct stat mine, theirs;
- app->why[0] = 0;
- if (pure_store_init(app->store) != PURE_OK) snprintf(app->why, sizeof(app->why), "Cannot initialize the store directory");
- else if (stat(app->store, &mine)) snprintf(app->why, sizeof(app->why), "Cannot read the store directory");
- for (unsigned b = 0, count = places(); !app->why[0] && b < count; ++b)
- if (&apps[b] != app && apps[b].running && !stat(apps[b].store, &theirs) && theirs.st_dev == mine.st_dev &&
- theirs.st_ino == mine.st_ino)
- snprintf(app->why, sizeof(app->why), "The store is %s's", apps[b].name);
- if (app->why[0]) return false;
+/* An app whose executor runs its module takes turns and requests: its
+ * outlets made for that module and its snapshot published; then the link
+ * streams to its inputs find them again. False, why in app->why, when its
+ * sends are wired wrong or an outlet does not start: it is then stopped,
+ * its store closed. */
+static bool resume_app(App *app) {
Owner *o = &app->owner;
Executor *ex = &o->executor;
- *ex = (Executor){0};
- if (!start_owner(app, all_hosts, host_count)) return false;
o->queue = app->queue;
bool wired = open_wires(app, app->name) && wire_replies(ex, app->module, app->replies, app->reply_count) &&
wire_seals(ex, app->module, app->seals, app->seal_count, seal_key ? own_public : NULL);
@@ -3731,13 +3724,32 @@ static bool start_app(App *app) {
return true;
}
-/* An app stops: no request taken from now on (its routes answer 503
- * stopped, link records and other apps' batches wait at their senders),
- * those taken applied, saved and answered, its batches with other apps
- * back, then its outlets closed (what they did not deliver stays in its
- * outbox) and its store closed. Its owner takes no turn after. */
-static void stop_app(App *app) {
- if (!app->running) return;
+/* An app of a host of apps starts in its place: its store (none another
+ * running app has), its owner on it, then it resumes. False, why in
+ * app->why, when it cannot: it stays stopped. */
+static bool start_app(App *app) {
+ struct stat mine, theirs;
+ app->why[0] = 0;
+ if (pure_store_init(app->store) != PURE_OK) snprintf(app->why, sizeof(app->why), "Cannot initialize the store directory");
+ else if (stat(app->store, &mine)) snprintf(app->why, sizeof(app->why), "Cannot read the store directory");
+ for (unsigned b = 0, count = places(); !app->why[0] && b < count; ++b)
+ if (&apps[b] != app && apps[b].running && !stat(apps[b].store, &theirs) && theirs.st_dev == mine.st_dev &&
+ theirs.st_ino == mine.st_ino)
+ snprintf(app->why, sizeof(app->why), "The store is %s's", apps[b].name);
+ if (app->why[0]) return false;
+ Executor *ex = &app->owner.executor;
+ *ex = (Executor){0};
+ if (!start_owner(app, all_hosts, host_count)) return false;
+ return resume_app(app);
+}
+
+/* An app goes quiet: it takes no request from now on (its routes answer
+ * 503 stopped, link records and other apps' batches wait at their
+ * senders), applies, saves and answers those it took, has its batches
+ * with other apps back, then closes its outlets (what they did not deliver
+ * stays in its outbox) and lets its checkpoint writer finish. Its owner
+ * takes no turn after; its executor and store are as they were. */
+static void quiet_app(App *app) {
app->running = false;
atomic_store_explicit(&app->live, NULL, memory_order_release);
Owner *o = &app->owner;
@@ -3764,18 +3776,62 @@ static void stop_app(App *app) {
if (!done) nanosleep(&(struct timespec){0, 1000000}, NULL);
}
close_outlets(ex, &app->past);
- stop_executor(ex);
- fold_run(ex, &app->past);
+ stop_writer(ex);
o->gather = false;
o->due = 0;
}
+/* An app stops: quiet, then its store closed. */
+static void stop_app(App *app) {
+ if (!app->running) return;
+ quiet_app(app);
+ Executor *ex = &app->owner.executor;
+ stop_executor(ex);
+ fold_run(ex, &app->past);
+}
+
+/* An app that runs changes in place, its line changed but not its store:
+ * quiet, then its module replaced in memory when it is another
+ * (pure_app_replace: its states, and the sends in its outbox, carried by
+ * name; nothing read again from the store), its budgets as the line says,
+ * and it resumes with outlets made anew. What its executor kept by input
+ * or output number starts anew. A module its states do not go into stops
+ * it, why in app->why (its store closed: a replace left uncertain starts
+ * again from what the store has). */
+static bool change_app(App *app) {
+ Executor *ex = &app->owner.executor;
+ quiet_app(app);
+ if (ex->module != app->module && pure_app_replace(&ex->app, app->module) != PURE_OK) {
+ snprintf(app->why, sizeof(app->why), "Its states do not go into the module%s%s", ex->app.why[0] ? ": " : "",
+ ex->app.why);
+ stop_executor(ex);
+ fold_run(ex, &app->past);
+ return false;
+ }
+ ex->module = app->module;
+ ex->show = app->show, ex->edit = app->edit;
+ memset(ex->next, 0, sizeof(ex->next));
+ memset(ex->trimmed, 0, sizeof(ex->trimmed));
+ memset(ex->held, 0, sizeof(ex->held));
+ memset(ex->link_output, 0, sizeof(ex->link_output));
+ memset(ex->link_trimmed, 0, sizeof(ex->link_trimmed));
+ memset(ex->local_trimmed, 0, sizeof(ex->local_trimmed));
+ memset(ex->outlet_from, 0, sizeof(ex->outlet_from));
+ ex->gone_seen = ex->app.sends_gone;
+ ex->answers_wait = false;
+ pure_app_memory(&ex->app, app->memory);
+ pure_app_work(&ex->app, atomic_load(&app->work));
+ return resume_app(app);
+}
+
/* A reading of --apps on SIGHUP: the host made as the file says. Apps whose
- * lines are gone stop and their routes go (404 no_app); those whose lines
- * changed, and those that do not run (they could not start, or a save of
- * theirs is uncertain), start (again) from their stores; new ones start in
- * places of their own; the others are left be. A line out for each
- * reading: each app, whether it runs (and why not), and what was done. */
+ * lines are gone stop and their routes go (404 no_app); a running app whose
+ * line changed but not its store changes in place (change_app); one whose
+ * store changed, one a save of which is uncertain, and one that does not
+ * run start (again) from their stores; new ones start in places of their
+ * own; the others are left be. A line out for each reading: each app,
+ * whether it runs (and why not), and what was done (kept, changed,
+ * restarted, started, removed). */
static unsigned readings;
static void read_again(const char *path) {
Line *lines = NULL;
@@ -3811,6 +3867,10 @@ static void read_again(const char *path) {
did[a] = "kept";
continue;
}
+ if (app->running && !uncertain && !strcmp(app->store, line->store)) {
+ did[a] = "changed"; /* in place, below */
+ continue;
+ }
did[a] = app->running ? "restarted" : "started";
stop_app(app);
}
@@ -3840,11 +3900,17 @@ static void read_again(const char *path) {
App *app = &apps[a];
bool same_module = app->module && app->module_path && !strcmp(app->module_path, line->module) &&
app->digest == line->digest && line->digest;
+ bool in_place = app->running; /* left running by the first pass: its store stays */
atomic_store(&app->gone, false); /* in the file again: stopped, not gone, until it starts */
if (!did[a]) did[a] = "started";
take_line(app, line);
app->why[0] = 0;
- if (same_module || load_module(app, app->module_path)) start_app(app); /* else stopped, with why */
+ if (!same_module && !load_module(app, app->module_path)) { /* stopped, with why */
+ if (in_place) stop_app(app);
+ continue;
+ }
+ if (in_place) change_app(app);
+ else start_app(app);
}
more_workers();
printf("{\"reading\":%u,\"apps\":[", readings);
diff --git a/tests/check.py b/tests/check.py
index 3876bf8..b746120 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -4035,12 +4035,14 @@ def server_apps_lifecycle(aup, pure, work):
"""Apps start, stop and change on a running host (docs/design/017,
stage 3): on SIGHUP it reads its --apps file again and says what it did
on a line. An app added answers; an app removed goes (404 no_app) and
- another process opens its store; apps change module while requests,
- another app's local sends and another node's link records come to them,
- the new modules' inputs at other numbers and taking the sends by name:
- a write answered 200 is applied once and one answered 503 stopped not at
- all, every send arrives once and in order, and the app left alone
- answers every request. An app that could not open its store starts at
+ another process opens its store; apps change module in place while
+ requests, another app's local sends and another node's link records
+ come to them and their views are read, the new modules' inputs at other
+ numbers and taking the sends by name, and the senders change too: a
+ write answered 200 is applied once and one answered 503 stopped not at
+ all, every send arrives once and in order, a view answers 200 or 503
+ stopped, and the app left alone answers every request; an app whose
+ store moves starts again on it. An app that could not open its store starts at
the next reading once it can; a file with a wrong line changes nothing;
after a kill -9 the host starts as the file says, the states as saved."""
import os
@@ -4150,6 +4152,13 @@ def server_apps_lifecycle(aup, pure, work):
if status != 200:
trouble.append(('c', status, why))
+ def viewer(): # the changing apps' views, lists among them, read as they change
+ while not done.is_set():
+ for path in ('/dst/views/count', '/dstl/views/sum', '/a/views/total', '/dst/views/in_order'):
+ status, why, _, _ = http(host.port, 'GET', path)
+ if status != 200 and (status, why) != (503, 'stopped'):
+ trouble.append((path, status, why))
+
def once(app, k):
deadline = time.time() + 30
while progress[app] < k and time.time() < deadline and not trouble:
@@ -4157,7 +4166,8 @@ def server_apps_lifecycle(aup, pure, work):
return dict(progress), answered[0]
workers = [threading.Thread(target=adder), threading.Thread(target=putter, args=(host.port, 'src')),
- threading.Thread(target=putter, args=(far.port, 'srcf')), threading.Thread(target=watcher)]
+ threading.Thread(target=putter, args=(far.port, 'srcf')), threading.Thread(target=watcher),
+ threading.Thread(target=viewer)]
for thread in workers:
thread.start()
during = []
@@ -4167,24 +4177,24 @@ def server_apps_lifecycle(aup, pure, work):
lines['dstl'] = f"dstl {module['life-taker-next']} {store['dstl']}"
_, did = reading(['a', 'src', 'dst', 'dstl', 'c'])
assert [did[name]['did'] for name in ('a', 'dst', 'dstl', 'c', 'src')] == \
- ['restarted', 'restarted', 'restarted', 'kept', 'kept'], did
+ ['changed', 'changed', 'changed', 'kept', 'kept'], did
during.append(once('src', 200))
lines['dst'] = f"dst {module['life-taker']} {store['dst']}" # and back: take's number moves again
_, did = reading(['a', 'src', 'dst', 'dstl', 'c'])
- assert did['dst']['did'] == 'restarted' and did['a']['did'] == 'kept', did
+ assert did['dst']['did'] == 'changed' and did['a']['did'] == 'kept', did
during.append(once('src', 300))
lines['src'] += ' queue=100000' # the sender starts again (its module as it was), its outbox going on
_, did = reading(['a', 'src', 'dst', 'dstl', 'c'])
- assert did['src']['did'] == 'restarted' and did['dst']['did'] == 'kept', did
+ assert did['src']['did'] == 'changed' and did['dst']['did'] == 'kept', did
during.append(once('srcf', 400))
(work / 'life-far.conf').write_text((work / 'life-far.conf').read_text().replace('\n', ' queue=100000\n'))
far.process.send_signal(signal.SIGHUP) # the far node's sender starts again, its link with it
ready, _, _ = select.select([far.process.stdout], [], [], 30)
- assert ready and json.loads(far.process.stdout.readline())['apps'][0]['did'] == 'restarted'
+ assert ready and json.loads(far.process.stdout.readline())['apps'][0]['did'] == 'changed'
for thread in workers[1:3]:
thread.join()
done.set()
- for thread in (workers[0], workers[3]):
+ for thread in (workers[0], workers[3], workers[4]):
thread.join()
assert not trouble, trouble[:5]
for put, adds in during: # each change came while the sends went
@@ -4200,6 +4210,10 @@ def server_apps_lifecycle(aup, pure, work):
total = 5 + answered[0]
assert number(host.port, '/a/views/total') == total and number(host.port, '/a/views/count') == 1 + answered[0], \
(number(host.port, '/a/views/total'), answered[0], refused[0])
+ # An app whose store moves starts again, on the store it moves to.
+ lines['c'] = f"c {module['life']} {work / 'life-store-c2'}"
+ _, did = reading(['a', 'src', 'dst', 'dstl', 'c'])
+ assert did['c']['did'] == 'restarted' and number(host.port, '/c/views/total') == 0, did
# An app whose store cannot be made yet stops alone; once it can, the next reading starts it.
lines['e'] = f"e {module['life']} {store['e']}"
os.chmod(work / 'life-ro', 0o500)
@@ -4228,7 +4242,7 @@ def server_apps_lifecycle(aup, pure, work):
finally:
host.stop()
far.stop()
- return {'readings': 8, 'far_readings': 1, 'sent': sent, 'answered_while_changing': answered[0],
+ return {'readings': 9, 'far_readings': 1, 'sent': sent, 'answered_while_changing': answered[0],
'refused_while_changing': refused[0], 'puts_at_changes': [put['src'] for put, _ in during]}