aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--serve/serve.c651
-rw-r--r--tests/check.py253
2 files changed, 762 insertions, 142 deletions
diff --git a/serve/serve.c b/serve/serve.c
index 3b713ef..e53d023 100644
--- a/serve/serve.c
+++ b/serve/serve.c
@@ -375,6 +375,12 @@ typedef struct Owner {
* with --module has no name (its routes start /views/ and so on); each of
* --apps has its own, the first segment of its routes. */
enum { APP_NAME = 32 };
+/* What an app did, added up over its starts (its executor's are its present run's). */
+typedef struct {
+ uint64_t bursts, events, ticks, memory_refused, windows_full, publications, answers, handed, checkpoints, delivered,
+ attempts;
+ PureLinkCounts sent;
+} Counts;
struct App {
char name[APP_NAME + 1]; /* never changed once the app is in its place: threads find it by name */
PureModule *module; /* the module it runs, or ran or was given last (NULL: none loaded) */
@@ -384,18 +390,22 @@ struct App {
* and snapshots may still point to it. */
_Atomic(const PureModule *) live;
_Atomic bool gone;
+ char *module_path; /* its module's file as its line gives it (a host of apps), and that file's digest */
+ uint64_t digest;
const char *store;
bool documents; /* the module serves /objects: it has show and edit */
bool owned; /* an owner runs it (states or outputs, or any of --apps); else the connection threads */
uint32_t show, edit;
size_t memory; /* its budgets (0: none): what its states hold, */
_Atomic uint64_t work; /* what an event or a view's evaluation may run */
+ size_t queue; /* the requests that may wait for its turn */
bool running; /* started (the main thread's word); else why says why */
char why[256];
unsigned home; /* the connection thread its connections move to (a host of apps) */
/* Its wiring in --apps (send=, seal=, reply=), as --send, --seal and --reply give the one app's. */
const char *sends[PURE_MODULE_INPUTS], *seals[PURE_MODULE_INPUTS], *replies[PURE_MODULE_INPUTS];
unsigned send_count, seal_count, reply_count;
+ Counts past; /* its runs' before the present one */
Owner owner;
};
static App *apps;
@@ -409,6 +419,7 @@ static unsigned places(void) { return atomic_load_explicit(&app_count, memory_or
static struct Retired { PureModule *module; struct Retired *next; } *retired_modules;
static bool named_apps; /* --apps: a route's first segment names its app */
static Host *all_hosts; /* the connection threads, for moving a connection to its app's home */
+static unsigned host_count;
static bool moved_home(Connection *c, size_t head_size);
static void schedule(Owner *owner);
@@ -1778,8 +1789,11 @@ static bool local_bind(Local *l, const PureModule *r, char *error, size_t size)
* receiver that does not run holds it (LOCAL_RETRY_MS), and one that runs
* another module now has its input found again first. */
static void local_hand(Executor *ex, Local *l, uint64_t now) {
+ pthread_mutex_lock(&l->from->lock);
+ bool leaving = l->from->stopping; /* its app stops: no batch more */
+ pthread_mutex_unlock(&l->from->lock);
pthread_mutex_lock(&l->lock);
- bool waits = l->out || !l->count || (l->retry && now < l->retry);
+ bool waits = leaving || l->out || !l->count || (l->retry && now < l->retry);
pthread_mutex_unlock(&l->lock);
if (waits) return;
Owner *to = l->to;
@@ -1854,6 +1868,7 @@ static void local_hand(Executor *ex, Local *l, uint64_t now) {
* again (after LOCAL_RETRY_MS when it had no room). The sender's turn
* comes either way. */
static void local_done(Local *l, uint64_t heard, bool stored, bool full) {
+ Owner *from = l->from; /* read first: with out false the sender may stop, and let l go */
pthread_mutex_lock(&l->lock);
l->out = false;
l->out_bytes = 0;
@@ -1863,7 +1878,6 @@ static void local_done(Local *l, uint64_t heard, bool stored, bool full) {
if (full) l->retry = now_ms() + LOCAL_RETRY_MS;
}
pthread_mutex_unlock(&l->lock);
- Owner *from = l->from;
pthread_mutex_lock(&from->lock);
from->delivered = true;
pthread_cond_signal(&from->waiting);
@@ -3369,14 +3383,22 @@ static bool wire_replies(Executor *ex, const PureModule *m, const char *const *r
* less the one the owner of an app with states takes. */
/* An app's owner, its executor started on the app's store, its snapshot
* published; false (why says why) when the app cannot start. */
+/* An owner's locks, made once, with its app's place (other apps' outlets
+ * lock them before the app starts, or when it never does). */
+static bool make_owner(Owner *owner) {
+ if (!owner->made && (pthread_mutex_init(&owner->lock, NULL) || pure_cond_monotonic(&owner->waiting) ||
+ pthread_mutex_init(&owner->published.lock, NULL)))
+ return false;
+ owner->made = true;
+ return true;
+}
+
static bool start_owner(App *app, Host *hosts, unsigned threads) {
Owner *owner = &app->owner;
- if (!owner->made && (pthread_mutex_init(&owner->lock, NULL) || pure_cond_monotonic(&owner->waiting) ||
- pthread_mutex_init(&owner->published.lock, NULL))) {
+ if (!make_owner(owner)) {
snprintf(app->why, sizeof(app->why), "Cannot make the owner's locks");
return false;
}
- owner->made = true;
if (!start_executor(&owner->executor, app)) return false;
owner->executor.reads_first = true;
owner->executor.published = &owner->published;
@@ -3385,79 +3407,191 @@ static bool start_owner(App *app, Host *hosts, unsigned threads) {
return true;
}
-/* --apps FILE: an app a line, NAME MODULE STORE and its budgets
- * (memory=BYTES work=N queue=N), # beginning a comment. An app whose
- * module does not load is kept, stopped, with why; false (and why on
- * stderr) when the file cannot be read or a line is not an app. */
-static bool read_apps(const char *path) {
+/* A line of --apps: an app as the file gives it, its strings its own. */
+typedef struct {
+ char name[APP_NAME + 1];
+ char *module, *store;
+ uint64_t digest; /* of the module file's bytes when it was read (0: it could not be) */
+ size_t memory, queue;
+ uint64_t work;
+ char *sends[PURE_MODULE_INPUTS], *seals[PURE_MODULE_INPUTS], *replies[PURE_MODULE_INPUTS];
+ unsigned send_count, seal_count, reply_count;
+ bool kept; /* its app runs as the line says: a reading leaves it be */
+} Line;
+
+static void let_go_line(Line *line) {
+ free(line->module);
+ free(line->store);
+ for (unsigned i = 0; i < line->send_count; ++i) free(line->sends[i]);
+ for (unsigned i = 0; i < line->seal_count; ++i) free(line->seals[i]);
+ for (unsigned i = 0; i < line->reply_count; ++i) free(line->replies[i]);
+}
+
+/* A file's bytes' digest (FNV-1a, 64 bits); 0 when it cannot be read. */
+static uint64_t file_digest(const char *path) {
+ FILE *file = fopen(path, "rb");
+ if (!file) return 0;
+ uint64_t digest = 14695981039346656037u;
+ unsigned char bytes[16384];
+ for (size_t n; (n = fread(bytes, 1, sizeof(bytes), file));)
+ for (size_t i = 0; i < n; ++i) digest = (digest ^ bytes[i]) * 1099511628211u;
+ bool read = !ferror(file);
+ fclose(file);
+ return read && digest ? digest : 1;
+}
+
+/* --apps FILE: an app a line, NAME MODULE STORE, its budgets (memory=BYTES
+ * work=N queue=N) and its wiring (send=, seal=, reply=), # beginning a
+ * comment. False, why in error, when the file cannot be read, a line is not
+ * an app, or none is. */
+static bool parse_apps(const char *path, Line **out, unsigned *count, char *error, size_t size) {
FILE *file = fopen(path, "r");
if (!file) {
- fprintf(stderr, "--apps %s: %s\n", path, strerror(errno));
+ snprintf(error, size, "--apps %s: %s", path, strerror(errno));
return false;
}
- char line[4096];
- unsigned at = 0;
+ Line *lines = NULL;
+ unsigned n = 0, at = 0;
+ char text[4096];
bool ok = true;
- while (ok && fgets(line, sizeof(line), file)) {
+ while (ok && fgets(text, sizeof(text), file)) {
++at;
- char *hash = strchr(line, '#'), *words[3 + 3 + 3 * PURE_MODULE_INPUTS], *rest = NULL;
+ char *hash = strchr(text, '#'), *words[3 + 3 + 3 * PURE_MODULE_INPUTS], *rest = NULL;
if (hash) *hash = 0;
- unsigned count = 0;
- for (char *word = strtok_r(line, " \t\r\n", &rest); word && count < sizeof(words) / sizeof(*words);
+ unsigned found = 0;
+ for (char *word = strtok_r(text, " \t\r\n", &rest); word && found < sizeof(words) / sizeof(*words);
word = strtok_r(NULL, " \t\r\n", &rest))
- words[count++] = word;
- if (!count) continue;
- App *grown = count >= 3 ? realloc(apps, (app_count + 1) * sizeof(*apps)) : NULL;
+ words[found++] = word;
+ if (!found) continue;
+ Line *grown = found >= 3 ? realloc(lines, (n + 1) * sizeof(*lines)) : NULL;
if (!grown) {
- fprintf(stderr, "--apps %s:%u: NAME MODULE STORE [memory=BYTES] [work=N] [queue=N] [send=OUTPUT=URL ...]\n",
- path, at);
+ snprintf(error, size, "--apps %s:%u: NAME MODULE STORE [memory=BYTES] [work=N] [queue=N] [send=OUTPUT=URL ...]",
+ path, at);
ok = false;
break;
}
- apps = grown;
- App *app = &apps[app_count];
- memset(app, 0, sizeof(*app));
+ lines = grown;
+ Line *line = &lines[n];
+ memset(line, 0, sizeof(*line));
size_t length = strlen(words[0]);
bool named = length <= APP_NAME;
for (size_t i = 0; named && i < length; ++i) {
char c = words[0][i];
named = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-';
}
- for (unsigned i = 0; named && i < app_count; ++i) named = strcmp(apps[i].name, words[0]) != 0;
+ for (unsigned i = 0; named && i < n; ++i) named = strcmp(lines[i].name, words[0]) != 0;
if (!named) {
- fprintf(stderr, "--apps %s:%u: a name is 1-32 of a-z 0-9 _ -, each app's its own\n", path, at);
+ snprintf(error, size, "--apps %s:%u: a name is 1-32 of a-z 0-9 _ -, each app's its own", path, at);
ok = false;
break;
}
- memcpy(app->name, words[0], length + 1);
- for (unsigned w = 3; ok && w < count; ++w) {
+ memcpy(line->name, words[0], length + 1);
+ ok = (line->module = strdup(words[1])) && (line->store = strdup(words[2]));
+ for (unsigned w = 3; ok && w < found; ++w) {
char *value = strchr(words[w], '=');
- uint64_t n;
+ uint64_t number_given;
if (value) *value++ = 0;
- if (value && !strcmp(words[w], "memory") && number(value, SIZE_MAX, &n)) app->memory = (size_t)n;
- else if (value && !strcmp(words[w], "work") && number(value, UINT64_MAX, &n)) app->work = n;
- else if (value && !strcmp(words[w], "queue") && number(value, SIZE_MAX, &n)) app->owner.queue = (size_t)n;
- else if (value && !strcmp(words[w], "send") && app->send_count < PURE_MODULE_INPUTS)
- ok = (app->sends[app->send_count++] = strdup(value)) != NULL;
- else if (value && !strcmp(words[w], "seal") && app->seal_count < PURE_MODULE_INPUTS)
- ok = (app->seals[app->seal_count++] = strdup(value)) != NULL;
- else if (value && !strcmp(words[w], "reply") && app->reply_count < PURE_MODULE_INPUTS)
- ok = (app->replies[app->reply_count++] = strdup(value)) != NULL;
+ if (value && !strcmp(words[w], "memory") && number(value, SIZE_MAX, &number_given)) line->memory = (size_t)number_given;
+ else if (value && !strcmp(words[w], "work") && number(value, UINT64_MAX, &number_given)) line->work = number_given;
+ else if (value && !strcmp(words[w], "queue") && number(value, SIZE_MAX, &number_given)) line->queue = (size_t)number_given;
+ else if (value && !strcmp(words[w], "send") && line->send_count < PURE_MODULE_INPUTS)
+ ok = (line->sends[line->send_count++] = strdup(value)) != NULL;
+ else if (value && !strcmp(words[w], "seal") && line->seal_count < PURE_MODULE_INPUTS)
+ ok = (line->seals[line->seal_count++] = strdup(value)) != NULL;
+ else if (value && !strcmp(words[w], "reply") && line->reply_count < PURE_MODULE_INPUTS)
+ ok = (line->replies[line->reply_count++] = strdup(value)) != NULL;
else {
- fprintf(stderr, "--apps %s:%u: an app takes memory=BYTES, work=N, queue=N, send=OUTPUT=URL, "
- "seal=OUTPUT=KEY and reply=OUTPUT=INPUT\n", path, at);
+ snprintf(error, size, "--apps %s:%u: an app takes memory=BYTES, work=N, queue=N, send=OUTPUT=URL, "
+ "seal=OUTPUT=KEY and reply=OUTPUT=INPUT", path, at);
ok = false;
}
}
- app->store = strdup(words[2]);
- app->owned = true; /* every app of a host of apps takes turns on the workers */
- if (!app->store) ok = false;
- else ++app_count;
- if (ok && load_module(app, words[1])) app->running = true; /* until its store says otherwise; else stopped, with why */
+ if (!ok && !*error) snprintf(error, size, "--apps %s:%u: out of memory", path, at);
+ if (!ok) {
+ let_go_line(line);
+ break;
+ }
+ line->digest = file_digest(line->module);
+ ++n;
}
fclose(file);
- if (ok && !app_count) fprintf(stderr, "--apps %s: no app\n", path);
- return ok && app_count;
+ if (ok && !n) {
+ snprintf(error, size, "--apps %s: no app", path);
+ ok = false;
+ }
+ if (!ok) {
+ for (unsigned i = 0; i < n; ++i) let_go_line(&lines[i]);
+ free(lines);
+ return false;
+ }
+ *out = lines;
+ *count = n;
+ return true;
+}
+
+/* An app takes its line: its module's file, store, budgets and wiring (the
+ * strings move to it; those it had go). Its module is not read here. */
+static void take_line(App *app, Line *line) {
+ free(app->module_path);
+ free((char *)app->store);
+ for (unsigned i = 0; i < app->send_count; ++i) free((char *)app->sends[i]);
+ for (unsigned i = 0; i < app->seal_count; ++i) free((char *)app->seals[i]);
+ for (unsigned i = 0; i < app->reply_count; ++i) free((char *)app->replies[i]);
+ app->module_path = line->module;
+ app->digest = line->digest;
+ app->store = line->store;
+ app->memory = line->memory;
+ atomic_store(&app->work, line->work);
+ app->queue = line->queue;
+ memcpy(app->sends, line->sends, sizeof(app->sends));
+ memcpy(app->seals, line->seals, sizeof(app->seals));
+ memcpy(app->replies, line->replies, sizeof(app->replies));
+ app->send_count = line->send_count, app->seal_count = line->seal_count, app->reply_count = line->reply_count;
+ *line = (Line){.kept = line->kept};
+ memcpy(line->name, app->name, sizeof(line->name));
+}
+
+static bool same_strings(const char *const *a, unsigned a_count, char *const *b, unsigned b_count) {
+ for (unsigned i = 0; a_count == b_count && i < a_count; ++i)
+ if (strcmp(a[i], b[i])) return false;
+ return a_count == b_count;
+}
+
+/* Whether an app runs as a line says: the same module (its file's bytes
+ * too), store, budgets and wiring. */
+static bool same_line(const App *app, const Line *line) {
+ return app->module_path && !strcmp(app->module_path, line->module) && app->digest == line->digest &&
+ !strcmp(app->store, line->store) && app->memory == line->memory && atomic_load(&app->work) == line->work &&
+ app->queue == line->queue && same_strings(app->sends, app->send_count, line->sends, line->send_count) &&
+ same_strings(app->seals, app->seal_count, line->seals, line->seal_count) &&
+ same_strings(app->replies, app->reply_count, line->replies, line->reply_count);
+}
+
+/* A host of apps' first reading: each app in a place, room left for apps
+ * started later (slots), its module read (an app whose module does not
+ * load is kept, stopped, with why). False, why on stderr, when the file is
+ * not one of apps. */
+static bool read_apps(const char *path, unsigned slots) {
+ Line *lines;
+ unsigned count;
+ char error[512];
+ if (!parse_apps(path, &lines, &count, error, sizeof(error))) {
+ fprintf(stderr, "%s\n", error);
+ return false;
+ }
+ app_slots = slots > count ? slots : count;
+ if (!(apps = calloc(app_slots, sizeof(*apps)))) return false;
+ for (unsigned i = 0; i < count; ++i) {
+ App *app = &apps[i];
+ memcpy(app->name, lines[i].name, sizeof(app->name));
+ app->owned = true; /* every app of a host of apps takes turns on the workers */
+ if (!make_owner(&app->owner)) return false;
+ take_line(app, &lines[i]);
+ load_module(app, app->module_path); /* else stopped, with why */
+ }
+ atomic_store(&app_count, count);
+ free(lines);
+ return true;
}
/* A JSON string's quotes and escapes around text. */
@@ -3481,6 +3615,272 @@ static unsigned auto_threads(bool owner) {
return owner && cpus > 1 ? cpus - 1 : cpus;
}
+
+/* ---- Apps that start and stop while the host runs (docs/design/017's stage 3) */
+
+/* An executor's counts, once it stopped, added to what its app did. */
+static void fold_run(const Executor *ex, Counts *c) {
+ c->bursts += ex->bursts, c->events += ex->burst_events, c->ticks += ex->ticks;
+ c->memory_refused += ex->memory_refused, c->windows_full += ex->windows_full, c->publications += ex->publications;
+ c->answers += ex->answers, c->handed += ex->handed, c->checkpoints += ex->checkpoints;
+}
+
+/* An executor's outlets closed, their counts added: what they did not
+ * deliver stays in the outbox, and goes again when the app starts again. */
+static void close_outlets(Executor *ex, Counts *c) {
+ for (unsigned i = 0; i < ex->wire_count; ++i) {
+ uint64_t delivered, attempts;
+ pure_wire_counts(ex->wires[i], &delivered, &attempts);
+ c->delivered += delivered, c->attempts += attempts;
+ pure_wire_close(ex->wires[i]);
+ }
+ for (unsigned i = 0; i < ex->link_count; ++i) {
+ PureLinkCounts k;
+ pure_link_counts(ex->links[i], &k);
+ c->sent.packets += k.packets, c->sent.records += k.records, c->sent.resent += k.resent, c->sent.lost += k.lost;
+ c->sent.acknowledged += k.acknowledged, c->sent.received += k.received, c->sent.stored += k.stored;
+ c->sent.refused += k.refused;
+ pure_link_close(ex->links[i]);
+ }
+ for (unsigned i = 0; i < ex->local_count; ++i) {
+ free(ex->locals[i]->batch);
+ pthread_mutex_destroy(&ex->locals[i]->lock);
+ free(ex->locals[i]);
+ }
+ ex->wire_count = ex->link_count = ex->local_count = 0;
+}
+
+/* The workers: an app's owner takes its turns on them, none its own
+ * thread. One for the one app of a host; for apps, as many as run, at most
+ * a CPU each (or --workers): made as apps start, never let go. The main
+ * thread's, and the control's once the host runs. */
+static pthread_t *workers_made;
+static unsigned worker_count, worker_room, workers_asked;
+static bool more_workers(void) {
+ unsigned owners = 0, cpus = online_cpus();
+ for (unsigned a = 0, count = places(); a < count; ++a) owners += apps[a].running && apps[a].owned;
+ unsigned want = !owners ? 0 : workers_asked ? workers_asked : !named_apps ? 1 : owners < cpus ? owners : cpus;
+ for (; worker_count < want; ++worker_count) {
+ if (worker_count == worker_room) {
+ unsigned room = worker_room ? 2 * worker_room : 4;
+ pthread_t *grown = realloc(workers_made, room * sizeof(*grown));
+ if (!grown) return false;
+ workers_made = grown, worker_room = room;
+ }
+ if (pthread_create(&workers_made[worker_count], NULL, run_worker, NULL)) return false;
+ pthread_setname_np(workers_made[worker_count], "pure-owner"); /* their CPU, seen apart */
+ }
+ 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;
+ 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);
+ if (!wired) snprintf(app->why, sizeof(app->why), "Its sends are wired wrong (said above)");
+ pthread_mutex_lock(&o->lock); /* it takes requests resolved with its module */
+ o->module = wired ? app->module : NULL;
+ o->stopping = !wired;
+ pthread_mutex_unlock(&o->lock);
+ if (wired) after_save(ex); /* the snapshot, and the sends stored before */
+ char error[256] = "";
+ for (unsigned i = 0; wired && i < ex->wire_count; ++i)
+ if (!pure_wire_start(ex->wires[i], wire_delivered, o)) snprintf(error, sizeof(error), "Cannot start a wire");
+ for (unsigned i = 0; wired && !*error && i < ex->link_count; ++i)
+ pure_link_start(ex->links[i], wire_delivered, o, error, sizeof(error));
+ if (!wired || *error) {
+ if (*error) snprintf(app->why, sizeof(app->why), "%s", error);
+ pthread_mutex_lock(&o->lock);
+ o->module = NULL;
+ o->stopping = true;
+ pthread_mutex_unlock(&o->lock);
+ close_outlets(ex, &app->past);
+ stop_executor(ex);
+ fold_run(ex, &app->past);
+ return false;
+ }
+ pthread_mutex_lock(&turns.lock);
+ o->live = true;
+ pthread_mutex_unlock(&turns.lock);
+ atomic_store(&app->gone, false);
+ atomic_store_explicit(&app->live, app->module, memory_order_release);
+ app->running = true;
+ if (link_listener) {
+ char prefix[APP_NAME + 2];
+ snprintf(prefix, sizeof(prefix), "%s/", app->name);
+ pure_link_listener_rebind(link_listener, prefix);
+ }
+ schedule(o); /* a first turn: its timers begin */
+ 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;
+ app->running = false;
+ atomic_store_explicit(&app->live, NULL, memory_order_release);
+ Owner *o = &app->owner;
+ Executor *ex = &o->executor;
+ pthread_mutex_lock(&o->lock);
+ o->stopping = true;
+ o->module = NULL;
+ pthread_cond_signal(&o->waiting); /* it may be gathering */
+ pthread_mutex_unlock(&o->lock);
+ for (bool done = false; !done;) {
+ pthread_mutex_lock(&o->lock);
+ bool taken = o->count > 0;
+ pthread_mutex_unlock(&o->lock);
+ bool out = false;
+ for (unsigned i = 0; i < ex->local_count; ++i) {
+ pthread_mutex_lock(&ex->locals[i]->lock);
+ out = out || ex->locals[i]->out;
+ pthread_mutex_unlock(&ex->locals[i]->lock);
+ }
+ pthread_mutex_lock(&turns.lock);
+ done = !taken && !out && !o->queued && !o->running;
+ if (done) o->live = false;
+ pthread_mutex_unlock(&turns.lock);
+ if (!done) nanosleep(&(struct timespec){0, 1000000}, NULL);
+ }
+ close_outlets(ex, &app->past);
+ stop_executor(ex);
+ fold_run(ex, &app->past);
+ o->gather = false;
+ o->due = 0;
+}
+
+/* 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. */
+static unsigned readings;
+static void read_again(const char *path) {
+ Line *lines = NULL;
+ unsigned count = 0, before = places();
+ char error[512];
+ ++readings;
+ const char **did = calloc(app_slots, sizeof(*did));
+ if (!did || !parse_apps(path, &lines, &count, error, sizeof(error))) {
+ if (!did) snprintf(error, sizeof(error), "out of memory");
+ printf("{\"reading\":%u,\"error\":", readings);
+ print_json(stdout, error);
+ printf("}\n");
+ fflush(stdout);
+ free(did);
+ return;
+ }
+ /* First what goes or changes stops: a store an app lets go another may take. */
+ for (unsigned a = 0; a < before; ++a) {
+ App *app = &apps[a];
+ if (atomic_load(&app->gone)) continue;
+ Line *line = NULL;
+ for (unsigned i = 0; i < count && !line; ++i)
+ if (!strcmp(lines[i].name, app->name)) line = &lines[i];
+ if (!line) {
+ stop_app(app);
+ atomic_store(&app->gone, true);
+ did[a] = "removed";
+ continue;
+ }
+ bool uncertain = app->running && __atomic_load_n(&app->owner.executor.app.uncertain, __ATOMIC_RELAXED);
+ if (app->running && !uncertain && same_line(app, line)) {
+ line->kept = true;
+ did[a] = "kept";
+ continue;
+ }
+ did[a] = app->running ? "restarted" : "started";
+ stop_app(app);
+ }
+ /* Each new name a place, before any starts: an app may send to one
+ * written after it. */
+ for (unsigned i = 0; i < count; ++i) {
+ unsigned a = 0, taken = places();
+ while (a < taken && strcmp(apps[a].name, lines[i].name)) ++a;
+ if (a < taken || taken == app_slots || !make_owner(&apps[a].owner)) continue;
+ App *app = &apps[a];
+ memcpy(app->name, lines[i].name, sizeof(app->name));
+ app->owned = true;
+ app->home = a % host_count;
+ atomic_store(&app->gone, false);
+ atomic_store_explicit(&app_count, taken + 1, memory_order_release); /* its name is there now */
+ did[a] = "started";
+ }
+ for (unsigned i = 0; i < count; ++i) {
+ Line *line = &lines[i];
+ unsigned a = 0, taken = places();
+ while (a < taken && strcmp(apps[a].name, line->name)) ++a;
+ if (line->kept) continue;
+ if (a == taken) {
+ fprintf(stderr, "{\"app\":\"%s\",\"error\":\"no place for it (--app-slots %u)\"}\n", line->name, app_slots);
+ continue;
+ }
+ App *app = &apps[a];
+ bool same_module = app->module && app->module_path && !strcmp(app->module_path, line->module) &&
+ app->digest == line->digest && line->digest;
+ 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 */
+ }
+ more_workers();
+ printf("{\"reading\":%u,\"apps\":[", readings);
+ bool first = true;
+ for (unsigned a = 0, taken = places(); a < taken; ++a) {
+ App *app = &apps[a];
+ if (atomic_load(&app->gone) && !did[a]) continue;
+ printf("%s{\"name\":", first ? "" : ",");
+ first = false;
+ print_json(stdout, app->name);
+ printf(",\"running\":%s", app->running ? "true" : "false");
+ if (did[a]) printf(",\"did\":\"%s\"", did[a]);
+ if (!app->running && !atomic_load(&app->gone)) {
+ printf(",\"why\":");
+ print_json(stdout, app->why);
+ }
+ printf("}");
+ }
+ printf("]}\n");
+ fflush(stdout);
+ for (unsigned i = 0; i < count; ++i) let_go_line(&lines[i]);
+ free(lines);
+ free(did);
+}
+
+/* The thread that reads --apps again: on SIGHUP, which every other thread
+ * blocks, until the host stops (it is sent one more then). */
+static void *run_apps(void *context) {
+ const char *path = context;
+ sigset_t hangup;
+ sigemptyset(&hangup);
+ sigaddset(&hangup, SIGHUP);
+ for (int signal_number; !sigwait(&hangup, &signal_number) && !atomic_load(&stopping);) read_again(path);
+ return NULL;
+}
+
int main(int argc, char **argv) {
static PureSurfaceHost surface;
/* What follows -- is the surface's: its library first, then its arguments. */
@@ -3539,14 +3939,12 @@ int main(int argc, char **argv) {
"--surface are for a host of one app (--module)\n", stderr);
return 2;
}
- if (!read_apps(apps_path)) return 2;
- /* Each app in a place of its own, room left for apps started later. */
- app_slots = slots > app_count ? slots : app_count;
- App *placed = calloc(app_slots, sizeof(*placed));
- if (!placed) return 1;
- memcpy(placed, apps, app_count * sizeof(*apps));
- free(apps);
- apps = placed;
+ /* SIGHUP is the reading thread's (run_apps): blocked before any thread is made, as they all inherit it. */
+ sigset_t hangup;
+ sigemptyset(&hangup);
+ sigaddset(&hangup, SIGHUP);
+ pthread_sigmask(SIG_BLOCK, &hangup, NULL);
+ if (!read_apps(apps_path, slots)) return 2;
named_apps = true;
} else {
if (!module_path || !store || !(apps = calloc(1, sizeof(*apps)))) goto usage;
@@ -3583,12 +3981,14 @@ int main(int argc, char **argv) {
pthread_t *thread = calloc(threads, sizeof(*thread));
if (!hosts || !thread || pure_cond_monotonic(&turns.wake)) return 1;
all_hosts = hosts;
+ host_count = threads;
for (unsigned a = 0; a < app_count; ++a) apps[a].home = a % threads; /* each app's connections meet on one thread */
/* Each app's store, and its owner: an app that cannot start stops alone
* (in a host of apps; the one app of a host stops the host). Two apps
* never share a store. */
- unsigned owners = 0;
- for (unsigned a = 0; a < app_count; ++a) {
+ for (unsigned a = 0; named_apps && a < app_count; ++a)
+ if (apps[a].module) start_app(&apps[a]); /* else stopped, with why */
+ for (unsigned a = 0; !named_apps && a < app_count; ++a) {
App *app = &apps[a];
if (!app->running) continue;
struct stat mine, theirs;
@@ -3602,11 +4002,10 @@ int main(int argc, char **argv) {
shared = true;
}
app->running = !app->why[0] && (!app->owned || start_owner(app, hosts, threads));
- if (!app->running && !named_apps) {
+ if (!app->running) {
fprintf(stderr, "%s\n", app->why);
return 1;
}
- owners += app->running && app->owned;
}
/* The sender's name: --name, or the module file's name without .pmod. */
char stem[PURE_SOURCE + 1];
@@ -3624,27 +4023,13 @@ int main(int argc, char **argv) {
memcpy(apps[0].replies, replies, reply_count * sizeof(*replies));
apps[0].send_count = send_count, apps[0].seal_count = seal_count, apps[0].reply_count = reply_count;
}
- /* Each app's wiring, its sends' source APP.OUTPUT (the one app's
- * --name.OUTPUT): an app of --apps wired wrong stops alone. */
- for (unsigned a = 0; a < app_count; ++a) {
- App *app = &apps[a];
- Executor *ex = &app->owner.executor;
- if (!app->running || !app->owned) continue;
- if (open_wires(app, named_apps ? app->name : 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))
- continue;
- if (!named_apps) return 2;
- snprintf(app->why, sizeof(app->why), "Its sends are wired wrong (said above)");
- for (unsigned i = 0; i < ex->wire_count; ++i) pure_wire_close(ex->wires[i]);
- for (unsigned i = 0; i < ex->link_count; ++i) pure_link_close(ex->links[i]);
- for (unsigned i = 0; i < ex->local_count; ++i) free(ex->locals[i]);
- ex->wire_count = ex->link_count = ex->local_count = 0;
- stop_executor(ex);
- app->running = false;
- --owners;
- }
- for (unsigned a = 0; a < app_count; ++a) {
+ /* The one app's wiring, its sends' source --name.OUTPUT (an app of
+ * --apps wired itself as it started, APP.OUTPUT). */
+ if (!named_apps && apps[0].owned &&
+ (!open_wires(&apps[0], name) || !wire_replies(&owner->executor, apps[0].module, apps[0].replies, apps[0].reply_count) ||
+ !wire_seals(&owner->executor, apps[0].module, apps[0].seals, apps[0].seal_count, seal_key ? own_public : NULL)))
+ return 2;
+ for (unsigned a = 0; !named_apps && a < app_count; ++a) {
App *app = &apps[a];
if (!app->running) continue;
if (app->owned) { /* it takes requests resolved with its module, and turns */
@@ -3654,7 +4039,7 @@ int main(int argc, char **argv) {
}
atomic_store(&app->live, app->module);
}
- for (unsigned a = 0; a < app_count; ++a) {
+ for (unsigned a = 0; !named_apps && a < app_count; ++a) {
Owner *o = &apps[a].owner;
if (!apps[a].running || !apps[a].owned) continue;
for (unsigned i = 0; i < o->executor.wire_count; ++i)
@@ -3673,19 +4058,9 @@ int main(int argc, char **argv) {
hosts[i].listener = hosts[i].epoll = -1;
if (!start_shard(&hosts[i], budget, &port)) return 1;
}
- /* The workers: an app's owner takes its turns on them, none its own
- * thread. One for the one app of a host; for apps, as many as run, at
- * most a CPU each. */
- if (!workers) workers = named_apps ? (owners < online_cpus() ? owners : online_cpus()) : 1;
- if (!workers) workers = 1;
- pthread_t *worker = calloc(workers, sizeof(*worker));
- if (!worker) return 1;
- for (unsigned i = 0; owners && i < workers; ++i) {
- if (pthread_create(&worker[i], NULL, run_worker, NULL)) { fputs("Cannot start a worker\n", stderr); return 1; }
- pthread_setname_np(worker[i], "pure-owner"); /* their CPU, seen apart */
- }
- for (unsigned a = 0; a < app_count; ++a)
- if (apps[a].running && apps[a].owned) schedule(&apps[a].owner); /* a first turn: its timers begin */
+ workers_asked = workers;
+ if (!more_workers()) { fputs("Cannot start a worker\n", stderr); return 1; }
+ if (!named_apps && apps[0].owned) schedule(owner); /* a first turn: its timers begin (an app of --apps had its) */
if (link_port) { /* records from the senders named, over UDP */
char error[256];
if ((!named_apps && !apps[0].owned) || !seal_key) {
@@ -3708,7 +4083,7 @@ int main(int argc, char **argv) {
}
}
if (named_apps) { /* each app, and why one stopped */
- printf("{\"port\":%u,\"threads\":%u,\"workers\":%u,\"apps\":[", port, threads, owners ? workers : 0);
+ printf("{\"port\":%u,\"threads\":%u,\"workers\":%u,\"apps\":[", port, threads, worker_count);
for (unsigned a = 0; a < app_count; ++a) {
printf("%s{\"name\":", a ? "," : "");
print_json(stdout, apps[a].name);
@@ -3727,6 +4102,10 @@ int main(int argc, char **argv) {
else printf("\"}\n");
}
fflush(stdout);
+ /* A host of apps reads its file again on SIGHUP, on a thread of its own. */
+ pthread_t reading;
+ if (named_apps && pthread_create(&reading, NULL, run_apps, (void *)apps_path)) { fputs("Cannot start a thread\n", stderr); return 1; }
+ if (named_apps) pthread_setname_np(reading, "pure-apps");
/* A surface reads the owner's snapshots and gives it events. */
if (surface_path && (named_apps || !apps[0].owned)) { fputs("--surface needs an app with states\n", stderr); return 2; }
if (surface_path && !start_surface(&surface, owner, surface_argc, surface_argv)) return 1;
@@ -3738,7 +4117,11 @@ int main(int argc, char **argv) {
surface_changed(&surface);
pthread_join(surface.thread, NULL);
}
- for (unsigned a = 0; a < app_count; ++a) { /* no gathering from now on */
+ if (named_apps) { /* a reading under way ends first */
+ pthread_kill(reading, SIGHUP);
+ pthread_join(reading, NULL);
+ }
+ for (unsigned a = 0, count = places(); a < count; ++a) { /* no gathering from now on */
Owner *o = &apps[a].owner;
if (!apps[a].running || !apps[a].owned) continue;
pthread_mutex_lock(&o->lock);
@@ -3750,7 +4133,7 @@ int main(int argc, char **argv) {
turns.stopping = true;
pthread_cond_broadcast(&turns.wake);
pthread_mutex_unlock(&turns.lock);
- for (unsigned i = 0; owners && i < workers; ++i) pthread_join(worker[i], NULL);
+ for (unsigned i = 0; i < worker_count; ++i) pthread_join(workers_made[i], NULL);
PureLinkCounts sent = {0}, heard = {0}; /* what links sent, and what the listener took */
pure_link_listener_close(link_listener, &heard); /* after the owner, which tells it what it stored */
size_t active = 0, retained = 0, state_bytes = 0;
@@ -3770,42 +4153,24 @@ int main(int argc, char **argv) {
expired += hosts[i].expired;
stop_shard(&hosts[i]);
}
- for (unsigned a = 0; a < app_count; ++a) {
- Executor *ex = &apps[a].owner.executor;
- if (!apps[a].running || !apps[a].owned) continue;
- bursts += ex->bursts;
- events += ex->burst_events;
- ticks += ex->ticks;
- memory_refused += ex->memory_refused;
- windows_full += ex->windows_full;
- publications += ex->publications;
- answers += ex->answers;
- for (unsigned i = 0; i < ex->wire_count; ++i) {
- uint64_t d, t;
- pure_wire_counts(ex->wires[i], &d, &t);
- delivered += d;
- attempts += t;
- pure_wire_close(ex->wires[i]); /* what was not delivered stays in the outbox */
- }
- for (unsigned i = 0; i < ex->link_count; ++i) {
- PureLinkCounts c;
- pure_link_counts(ex->links[i], &c);
- sent.packets += c.packets, sent.records += c.records, sent.resent += c.resent, sent.lost += c.lost;
- sent.acknowledged += c.acknowledged, sent.received += c.received, sent.stored += c.stored;
- sent.refused += c.refused;
- pure_link_close(ex->links[i]); /* what was not stored stays in the outbox */
- }
- for (unsigned i = 0; i < ex->local_count; ++i) {
- free(ex->locals[i]->batch);
- pthread_mutex_destroy(&ex->locals[i]->lock);
- free(ex->locals[i]);
+ for (unsigned a = 0, count = places(); a < count; ++a) { /* each app's runs, the present one stopped now */
+ App *app = &apps[a];
+ Counts *c = &app->past;
+ if (app->running && app->owned) {
+ Executor *ex = &app->owner.executor;
+ state_bytes += pure_app_state_bytes(&ex->app);
+ close_outlets(ex, c); /* what was not delivered stays in the outbox */
+ stop_executor(ex);
+ fold_run(ex, c);
}
- handed += ex->handed;
- state_bytes += pure_app_state_bytes(&ex->app);
- stop_executor(ex);
- checkpoints += ex->checkpoints;
- free(apps[a].owner.jobs);
- free(apps[a].owner.spare);
+ free(app->owner.jobs);
+ free(app->owner.spare);
+ bursts += c->bursts, events += c->events, ticks += c->ticks, memory_refused += c->memory_refused;
+ windows_full += c->windows_full, publications += c->publications, answers += c->answers, handed += c->handed;
+ checkpoints += c->checkpoints, delivered += c->delivered, attempts += c->attempts;
+ sent.packets += c->sent.packets, sent.records += c->sent.records, sent.resent += c->sent.resent;
+ sent.lost += c->sent.lost, sent.acknowledged += c->sent.acknowledged, sent.received += c->sent.received;
+ sent.stored += c->sent.stored, sent.refused += c->sent.refused;
}
unsigned long groups, grouped;
pure_store_group_counts(&groups, &grouped);
@@ -3829,21 +4194,21 @@ int main(int argc, char **argv) {
windows_full);
if (named_apps) { /* what each app did */
fprintf(stderr, ",\"workers\":%u,\"moved_home\":%" PRIu64 ",\"handed\":%" PRIu64 ",\"apps\":[",
- owners ? workers : 0, moved, handed);
- for (unsigned a = 0; a < app_count; ++a) {
- const Owner *o = &apps[a].owner;
+ worker_count, moved, handed);
+ for (unsigned a = 0, count = places(); a < count; ++a) {
+ const App *app = &apps[a];
fprintf(stderr, "%s{\"name\":", a ? "," : "");
- print_json(stderr, apps[a].name);
+ print_json(stderr, app->name);
fprintf(stderr, ",\"running\":%s,\"turns\":%" PRIu64 ",\"bursts\":%" PRIu64 ",\"events\":%" PRIu64
- ",\"busy\":%" PRIu64 ",\"memory_refused\":%" PRIu64 "}", apps[a].running ? "true" : "false",
- o->turns, o->executor.bursts, o->executor.burst_events, o->busy, o->executor.memory_refused);
+ ",\"busy\":%" PRIu64 ",\"memory_refused\":%" PRIu64 "}", app->running ? "true" : "false",
+ app->owner.turns, app->past.bursts, app->past.events, app->owner.busy, app->past.memory_refused);
}
fputc(']', stderr);
}
fputs("}\n", stderr);
free(hosts);
free(thread);
- free(worker);
+ free(workers_made);
return 0;
usage:
fputs("pure-serve --module FILE.pmod --store DIRECTORY [--port 8767] [--threads 1|N|auto] [--budget BYTES] "
@@ -3851,13 +4216,15 @@ usage:
"[--send-ca FILE] [--reply OUTPUT=INPUT ...] [--seal OUTPUT=KEY ...] [--key FILE] [--sealed-only] "
"[--sender NAME=KEY ...] [--link-port PORT] [--senders FILE] [--link-idle-ms 30000] [--memory BYTES] [--work N] "
"[--window 4194304] [--surface LIBRARY [-- ARGUMENTS]]\n"
- "pure-serve --apps FILE [--port 8767] [--threads 1|N|auto] [--workers N] [--budget BYTES] [--connections 1024] "
+ "pure-serve --apps FILE [--port 8767] [--threads 1|N|auto] [--workers N] [--app-slots 256] [--budget BYTES] [--connections 1024] "
"[--timeout-ms 10000] [--key FILE] [--sealed-only] [--sender NAME=KEY ...] [--link-port PORT] [--senders FILE] "
"[--send-ca FILE] [--window BYTES]\n"
" --apps FILE: a host of apps (docs/design/017-apps-on-one-host.md), a line each:\n"
" NAME MODULE.pmod STORE [memory=BYTES] [work=N] [queue=N] [send=OUTPUT=URL ...] [seal=OUTPUT=KEY ...]\n"
" [reply=OUTPUT=INPUT ...]; its routes start /NAME/, its sends' sources are NAME.OUTPUT, and a\n"
- " --link-port takes its inputs as NAME/INPUT\n"
+ " --link-port takes its inputs as NAME/INPUT. On SIGHUP the host reads FILE again: the apps of lines\n"
+ " gone stop, those of new or changed lines start (again), from their stores; a line out says what it did\n"
+ " --app-slots N: the apps a host of apps holds at most, those started later included\n"
" --workers N: the threads the apps' owners take turns on (1 for one app; for apps, one a CPU at most)\n"
" --send OUTPUT=pure://host:port/INPUT: to a Pure node's --link-port, the way between nodes (UDP, no\n"
" connection, docs/design/012-link-protocol.md; its key by --seal OUTPUT=KEY)\n"
diff --git a/tests/check.py b/tests/check.py
index 91fa9b9..3876bf8 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -3980,6 +3980,258 @@ def server_apps_links(aup, pure, work):
return {'sent': sent, 'killed_at': killed_at, 'handed': near_stats['handed'], 'small_held': held}
+LIFE_SOURCE = """state n = 0
+state hits = 0
+input add = message { step: number }
+on add {
+ n = n + add.step
+ hits = hits + 1
+}
+view total = n
+view count = hits
+"""
+# Its next version: another input before add (add's number moves), and a state more.
+LIFE_NEXT = """state twice = 0
+state n = 0
+state hits = 0
+input reset = message { to: number }
+input add = message { step: number }
+on reset {
+ n = reset.to
+}
+on add {
+ n = n + add.step
+ hits = hits + 1
+ twice = twice + 2 * add.step
+}
+view total = n
+view count = hits
+view doubled = twice
+"""
+LIFE_SENDER = """state n = 0
+input put = message { k: u32 }
+output here = message { k: u32 }
+output far = message { k: u32 }
+on put {
+ n = n + 1
+ send here { k: put.k }
+ send far { k: put.k }
+}
+view count = n
+"""
+# A taker's next version: an input before take, which takes a field more (the sends by name).
+LIFE_TAKER_NEXT = LINKS_TAKER.replace('input take = message { k: u32 }', """state pokes = 0
+input poke = message { x: u32 }
+input take = message {
+ note: text
+ k: u32
+}
+on poke {
+ pokes = pokes + poke.x
+}""")
+
+
+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
+ 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
+ import signal
+ import select
+ import socket
+ import threading
+ import time
+ for name, text in (('life', LIFE_SOURCE), ('life-next', LIFE_NEXT), ('life-sender', LIFE_SENDER),
+ ('life-taker', LINKS_TAKER), ('life-taker-next', LIFE_TAKER_NEXT)):
+ (work / f'{name}.aup').write_text(text)
+ compile_module(aup, work / f'{name}.aup', work / f'{name}.pmod')
+ serve = pure / 'build/pure-serve'
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
+ probe.bind(('127.0.0.1', 0))
+ link_port = probe.getsockname()[1]
+ keys = {}
+ for side in ('host', 'far'):
+ (work / f'life-{side}-key.conf').write_text(f"x {work / 'life.pmod'} {work / f'life-keys-{side}'}\n")
+ host = HttpHost([serve, '--apps', work / f'life-{side}-key.conf', '--key', work / f'life-{side}.key'])
+ keys[side] = host.info['public_key']
+ host.stop()
+ module = {name: work / f'{name}.pmod' for name in ('life', 'life-next', 'life-sender', 'life-taker', 'life-taker-next')}
+ store = {name: work / f'life-store-{name}' for name in ('a', 'b', 'c', 'src', 'dst', 'dstl', 'e', 'srcf')}
+ (work / 'life-ro').mkdir()
+ store['e'] = work / 'life-ro' / 'e'
+ lines = {'a': f"a {module['life']} {store['a']}", 'b': f"b {module['life']} {store['b']}",
+ 'src': f"src {module['life-sender']} {store['src']} send=here=dst/take",
+ 'dst': f"dst {module['life-taker']} {store['dst']}", 'dstl': f"dstl {module['life-taker']} {store['dstl']}"}
+ conf = work / 'life.conf'
+
+ def write(names):
+ conf.write_text(''.join(lines[name] + '\n' for name in names))
+
+ write(['a', 'b', 'src', 'dst', 'dstl'])
+ args = [serve, '--apps', conf, '--key', work / 'life-host.key', '--link-port', link_port, '--threads', 2,
+ '--sender', f"srcf.far={keys['far']}"]
+ host = HttpHost(args)
+ (work / 'life-far.conf').write_text(f"srcf {module['life-sender']} {store['srcf']} "
+ f"send=far=pure://127.0.0.1:{link_port}/dstl/take seal=far={keys['host']}\n")
+ far = HttpHost([serve, '--apps', work / 'life-far.conf', '--key', work / 'life-far.key'])
+
+ def reading(names=None):
+ """The host reads its file again: what it said."""
+ if names is not None:
+ write(names)
+ host.process.send_signal(signal.SIGHUP)
+ ready, _, _ = select.select([host.process.stdout], [], [], 30)
+ assert ready, 'no reading'
+ said = json.loads(host.process.stdout.readline())
+ return said, {app['name']: app for app in said.get('apps', [])}
+
+ def number(port, path):
+ status, why, _, body = http(port, 'GET', path)
+ assert status == 200, (path, status, why)
+ return int(body) if body not in (b'true', b'false') else body == b'true'
+
+ sent = 600
+ try:
+ # Added: it answers. Removed: its routes go, and its store is let go.
+ assert http(host.port, 'POST', '/a/inputs/add?step=5')[0] == 200
+ assert http(host.port, 'POST', '/b/inputs/add?step=7')[0] == 200
+ lines['c'] = f"c {module['life']} {store['c']}"
+ _, did = reading(['a', 'b', 'src', 'dst', 'dstl', 'c'])
+ assert did['c'] == {'name': 'c', 'running': True, 'did': 'started'} and did['a']['did'] == 'kept', did
+ assert http(host.port, 'POST', '/c/inputs/add?step=2')[0] == 200 and number(host.port, '/c/views/total') == 2
+ _, did = reading(['a', 'src', 'dst', 'dstl', 'c'])
+ assert did['b'] == {'name': 'b', 'running': False, 'did': 'removed'}, did
+ assert http(host.port, 'GET', '/b/views/total')[:2] == (404, 'no_app')
+ other = HttpHost([serve, '--module', module['life'], '--store', store['b']])
+ try:
+ assert number(other.port, '/views/total') == 7
+ finally:
+ other.stop()
+ # Changed under load: a, dst (another app's local sends) and dstl (another node's link records) take
+ # modules whose inputs are at other numbers, the takers' by name; then the senders start again, src
+ # (its local outlet) and, on the far node, srcf (its link); c, left alone, answers every request. Each
+ # change comes while the sends go (as far as each putter got: more than none, less than all).
+ answered, refused, done = [0], [0], threading.Event()
+ progress, trouble = {'src': 0, 'srcf': 0}, []
+
+ def adder():
+ while not done.is_set():
+ status, why, _, _ = http(host.port, 'POST', '/a/inputs/add?step=1')
+ if status == 200:
+ answered[0] += 1
+ elif (status, why) == (503, 'stopped'):
+ refused[0] += 1
+ else:
+ trouble.append(('a', status, why))
+
+ def putter(port, app):
+ for k in range(1, sent + 1):
+ status, why, _, _ = http(port, 'POST', f'/{app}/inputs/put?k={k}')
+ while (status, why) == (503, 'stopped'): # its app starting again: not applied, sent again
+ time.sleep(0.005)
+ status, why, _, _ = http(port, 'POST', f'/{app}/inputs/put?k={k}')
+ if status != 200:
+ trouble.append((app, k, status, why))
+ return
+ progress[app] = k
+ time.sleep(0.002)
+
+ def watcher():
+ while not done.is_set():
+ status, why, _, _ = http(host.port, 'GET', '/c/views/total')
+ if status != 200:
+ trouble.append(('c', status, why))
+
+ def once(app, k):
+ deadline = time.time() + 30
+ while progress[app] < k and time.time() < deadline and not trouble:
+ time.sleep(0.005)
+ 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)]
+ for thread in workers:
+ thread.start()
+ during = []
+ during.append(once('src', 100))
+ lines['a'] = f"a {module['life-next']} {store['a']}"
+ lines['dst'] = f"dst {module['life-taker-next']} {store['dst']}"
+ 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
+ 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
+ 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
+ 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'
+ for thread in workers[1:3]:
+ thread.join()
+ done.set()
+ for thread in (workers[0], workers[3]):
+ thread.join()
+ assert not trouble, trouble[:5]
+ for put, adds in during: # each change came while the sends went
+ assert 0 < put['src'] < sent and 0 < put['srcf'] < sent and adds > 0, during
+ whole = [sent, sent * (sent + 1) // 2, True]
+ deadline = time.time() + 30
+ while time.time() < deadline and ([number(host.port, f'/dst/views/{v}') for v in ('count', 'sum', 'in_order')] != whole or
+ [number(host.port, f'/dstl/views/{v}') for v in ('count', 'sum', 'in_order')] != whole):
+ time.sleep(0.05)
+ for app in ('dst', 'dstl'):
+ got = [number(host.port, f'/{app}/views/{v}') for v in ('count', 'sum', 'in_order')]
+ assert got == whole, (app, got)
+ 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 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)
+ try:
+ _, did = reading(['a', 'src', 'dst', 'dstl', 'c', 'e'])
+ finally:
+ os.chmod(work / 'life-ro', 0o700)
+ assert not did['e']['running'] and 'store' in did['e']['why'] and did['c']['did'] == 'kept', did
+ _, did = reading()
+ assert did['e'] == {'name': 'e', 'running': True, 'did': 'started'}, did
+ assert http(host.port, 'POST', '/e/inputs/add?step=3')[0] == 200
+ # A file with a wrong line changes nothing.
+ good = conf.read_text()
+ conf.write_text(good + 'two words\n') # NAME MODULE STORE at least
+ said, _ = reading()
+ assert 'error' in said and number(host.port, '/a/views/total') == total, said
+ conf.write_text(good)
+ # After a kill -9 the host starts as the file says, its states as saved.
+ host.process.send_signal(signal.SIGKILL)
+ host.process.wait()
+ host = HttpHost(args)
+ assert all(app['running'] for app in host.info['apps']), host.info
+ assert sorted(app['name'] for app in host.info['apps']) == ['a', 'c', 'dst', 'dstl', 'e', 'src'], host.info
+ assert number(host.port, '/a/views/total') == total and number(host.port, '/e/views/total') == 3
+ assert [number(host.port, f'/dst/views/{v}') for v in ('count', 'sum', 'in_order')] == whole
+ finally:
+ host.stop()
+ far.stop()
+ return {'readings': 8, 'far_readings': 1, 'sent': sent, 'answered_while_changing': answered[0],
+ 'refused_while_changing': refused[0], 'puts_at_changes': [put['src'] for put, _ in during]}
+
+
def main():
global EDITOR
parser = argparse.ArgumentParser()
@@ -4031,6 +4283,7 @@ def main():
report['server_work'] = server_work(aup, pure, work)
report['server_apps'] = server_apps(aup, pure, work)
report['server_apps_links'] = server_apps_links(aup, pure, work)
+ report['server_apps_lifecycle'] = server_apps_lifecycle(aup, pure, work)
print(json.dumps(report))