diff options
| -rw-r--r-- | serve/serve.c | 874 | ||||
| -rw-r--r-- | tests/check.py | 117 |
2 files changed, 750 insertions, 241 deletions
diff --git a/serve/serve.c b/serve/serve.c index b283874..a7a3d6c 100644 --- a/serve/serve.c +++ b/serve/serve.c @@ -60,6 +60,7 @@ #include <sys/epoll.h> #include <sys/eventfd.h> #include <sys/socket.h> +#include <sys/stat.h> #include <sys/uio.h> #include <time.h> #include <unistd.h> @@ -74,7 +75,8 @@ typedef struct Host Host; typedef enum { ROUTE_NONE, ROUTE_OBJECT, ROUTE_VIEW, ROUTE_INPUT, ROUTE_QUERY } RouteKind; /* A view's (or a query's) arguments come as the request's query: argued * says so, and given is its text, kept on the request's work slot. */ -typedef struct { RouteKind kind; uint32_t target; size_t from, count; bool argued; const char *given; } Route; +typedef struct App App; +typedef struct { RouteKind kind; uint32_t target; size_t from, count; bool argued; const char *given; App *app; } Route; /* This node's key, for deliveries sealed to it (--key), and whether it * takes only those (--sealed-only), or only those from the senders named, @@ -114,6 +116,7 @@ typedef struct Connection { uint64_t deadline; char key[65]; /* the document's key, or the view's or input's name */ RouteKind route; + App *app; /* the app the path names (NULL: none of the host's) */ /* An app input's message: its fixed fields from the query, which fields * were given there, and whether the body is a JSON object of the rest. * Its ingress is [u32le payload size][payload]; with JSON it is [u32le @@ -166,6 +169,7 @@ typedef struct { PureFault fault; /* a failed event's */ const char *type; /* a view's content type */ struct LinkJob *link; /* records a link delivered (link/link.h) */ + const char *refused; /* refused by the host (503 and this name), not applied */ uint64_t heard; /* then the source's last number heard after them */ bool gap; /* and whether they stopped at a gap */ } Applied; @@ -305,7 +309,7 @@ typedef struct LinkJob { PureLinkListener *listener; void *token; unsigned char typedef struct { Host *host; PureEffect effect; Route route; Waiter *waiter; LinkJob *link; } Job; /* The owner of an app with states served by several connection threads: - * their requests wait here, and it runs them in bursts, one save each + * their requests wait here, and a worker runs them in bursts, one save each * however many threads they came from, handing each thread back its * applied requests to answer (and a surface its events). */ typedef struct Owner { @@ -319,14 +323,53 @@ typedef struct Owner { bool delivered; /* a wire delivered a send: trim it */ Host *hosts; unsigned host_count; + /* Its turns on the workers (docs/design/017-apps-on-one-host.md): queued + * with work to do, or running on one; again when work came while it ran; + * due when its next timer is (the monotonic clock in ms, 0: none). */ + struct Owner *next; + bool queued, running, again; + uint64_t due, turns; + size_t queue; /* how many requests may wait for its turn (0: any number) */ + uint64_t busy; /* requests refused for that */ + /* Between its turns: whether the last one saved (it then gathers the + * clients it answered), the arrivals when it handed them back, and the + * jobs' spare array. */ + bool gather; + uint64_t answered_at; + Job *spare; + size_t spare_capacity; } Owner; -struct Host { - const PureModule *module; +/* An app of the host (docs/design/017-apps-on-one-host.md): its module, + * store and budgets, and the owner that applies its events on the workers, + * taking turns with the host's other apps. The one app of a host started + * 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 }; +struct App { + char name[APP_NAME + 1]; + PureModule module; + const char *store; bool documents; /* the module serves /objects: it has show and edit */ - Executor local; /* the app, when this thread runs it */ - Owner *owner; /* or the owner that runs it */ - PureApp reader; /* shows the owner's snapshot here */ + 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, */ + uint64_t work; /* what an event or a view's evaluation may run */ + bool running; /* started; else its routes answer 503 stopped, and why says why */ + char why[256]; + Owner owner; +}; +static App *apps; +static unsigned app_count; +static bool named_apps; /* --apps: a route's first segment names its app */ +static void schedule(Owner *owner); + +struct Host { + Executor local; /* the one app of the host, when this thread runs it (documents alone) */ + PureApp *readers; /* by app: shows its owner's snapshots here */ + PureSnapshot **snapshots; /* by app: the snapshot a round reads views from, once */ + uint64_t round, *read_in, *handed_in; /* by app: the round it was read in, its requests handed in */ + unsigned *touched; /* the apps read in this round */ Applied *views; /* a round's views shown here, answered together */ size_t views_capacity; uint64_t shown; /* views shown from snapshots */ @@ -606,6 +649,15 @@ static bool parse_head(Connection *c, char *head, size_t size, char **query) { *version++ = 0; if (strcmp(version, "HTTP/1.1")) return false; if ((*query = strchr(path, '?'))) *(*query)++ = 0; + /* A host of apps (--apps): the path's first segment names one, and the rest routes in it. */ + c->app = named_apps ? NULL : &apps[0]; + if (named_apps && *path == '/') { + const char *end = strchr(path + 1, '/'); + size_t length = end ? (size_t)(end - path - 1) : 0; + for (unsigned i = 0; end && i < app_count && !c->app; ++i) + if (strlen(apps[i].name) == length && !memcmp(apps[i].name, path + 1, length)) c->app = &apps[i]; + if (end) path += 1 + length; + } static const struct { const char *prefix; RouteKind kind; } routes[] = { {"/objects/", ROUTE_OBJECT}, {"/views/", ROUTE_VIEW}, {"/inputs/", ROUTE_INPUT}, {"/queries/", ROUTE_QUERY}}; const char *key = ""; @@ -729,13 +781,20 @@ static long percent_decoded(char *text, size_t length) { * taken from the query (%XX decoded, as a view's arguments are) into * c->fields. NULL, or the error to answer (404 for a name the module lacks, * 400 otherwise). */ -static const char *resolve(const Host *host, Connection *c, char *query, Route *route, int *status) { - const PureModule *m = host->module; +static const char *resolve(Connection *c, char *query, Route *route, int *status) { + App *app = c->app; *status = 404; c->field_bytes = 0; memset(c->given, 0, sizeof(c->given)); - if (route->kind == ROUTE_NONE || (route->kind == ROUTE_OBJECT && !host->documents) || - (route->kind == ROUTE_QUERY && !host->owner)) return "route"; /* a query reads an app's states */ + if (!app) return "no_app"; + if (!app->running) { + *status = 503; + return "stopped"; + } + const PureModule *m = &app->module; + route->app = app; + if (route->kind == ROUTE_NONE || (route->kind == ROUTE_OBJECT && !app->documents) || + (route->kind == ROUTE_QUERY && !app->owned)) return "route"; /* a query reads an app's states */ if (route->kind == ROUTE_OBJECT) return NULL; if (route->kind == ROUTE_QUERY) { /* the view is the query module's, found when shown */ route->from = 0; @@ -770,7 +829,7 @@ static const char *resolve(const Host *host, Connection *c, char *query, Route * bool message = shape && shape->kind == PURE_KIND_MESSAGE; if (message && shape->keyed) return "keyed"; if (*c->source && !message) return "delivery"; - if (!host->owner) c->source[0] = 0; /* an app without states or outputs applies it as a message */ + if (!app->owned) c->source[0] = 0; /* an app without states or outputs applies it as a message */ if (shape) { /* A body is a text input's text, a message's one variable field (rows * in their list's stored form), or a JSON object of a message's fields @@ -841,14 +900,14 @@ enum { DELIVERY = 80 }; * into the prefix; strings stay in the body, and the table says where. * NULL, or the error to answer. */ static const char *convert(Connection *c) { - const PureInputShape *shape = &c->host->module->input[c->input]; + const PureInputShape *shape = &(&c->app->module)->input[c->input]; uint32_t delivered = get_u32(c->ingress) & DELIVERED; size_t table = 8 * (size_t)shape->field_count, payload = c->batch * c->field_bytes + c->body_size; unsigned char *tables = c->ingress + 4 + (delivered ? DELIVERY : 0), *prefixes = tables + c->batch * table; unsigned char *json = c->body; size_t size = c->body_size, at = pure_json_space(json, 0, size); /* A message's rows go after the body: they are encoded, then placed. */ - PureJsonMessage b = {.module = c->host->module, .shape = shape, .given = c->given, .rows_base = payload}; + PureJsonMessage b = {.module = (&c->app->module), .shape = shape, .given = c->given, .rows_base = payload}; const char *why = NULL; /* A batch is an array of its messages' objects. */ if (c->batch > 1) { @@ -868,7 +927,7 @@ static const char *convert(Connection *c) { * member naming no field is refused all the same: it would be dropped. */ for (uint32_t f = 0; !why && f < shape->field_count; ++f) { if (b.seen[f] || c->given[f]) continue; - const PureField *field = &c->host->module->field[shape->field_start + f]; + const PureField *field = &(&c->app->module)->field[shape->field_start + f]; if (!delivered || field->type == PURE_FIELD_ROWS) why = "missing_field"; else if (pure_field_fixed(field->type)) memset(b.payload + field->offset, 0, pure_field_size(field->type)); @@ -979,12 +1038,15 @@ static void start_request(Connection *c, size_t head_size) { ++host->requests; Route route = {.kind = c->route}; int status; - const char *why = resolve(host, c, query, &route, &status); - if (why) { reject(c, status, status == 404 ? "Not Found" : "Bad Request", why); return; } + const char *why = resolve(c, query, &route, &status); + if (why) { + reject(c, status, status == 404 ? "Not Found" : status == 503 ? "Service Unavailable" : "Bad Request", why); + return; + } /* A document's PUT carries its body; an input's event its payload's size, * the payload (the fixed fields, then the body), and room for its JSON. */ bool write = route.kind == ROUTE_INPUT || c->put; - size_t table = route.kind == ROUTE_INPUT && c->json ? 8 * (size_t)host->module->input[c->input].field_count : 0; + size_t table = route.kind == ROUTE_INPUT && c->json ? 8 * (size_t)c->app->module.input[c->input].field_count : 0; size_t lead = 4 + (route.kind == ROUTE_INPUT && *c->source ? DELIVERY : 0); /* A batch has a table and a prefix for each of its messages. */ size_t input_size = route.kind == ROUTE_INPUT ? lead + c->batch * (table + c->field_bytes) + c->body_size : @@ -1769,7 +1831,10 @@ static void answer_applied(Host *host, Applied *a) { if (result.status == PURE_OK && a->extents) respond_typed(c, 200, "OK", "none", result.revision, a->type ? a->type : "application/octet-stream", a->extents, a->extent_count, result.body.size); - else if (a->route != ROUTE_OBJECT) answer_app(c, result, a->route == ROUTE_VIEW, a->fault, a->type); + else if (a->refused) { + respond(c, 503, "Service Unavailable", a->refused, 0, (PureSpan){0}); + release_ticket(c); + } else if (a->route != ROUTE_OBJECT) answer_app(c, result, a->route == ROUTE_VIEW, a->fault, a->type); else answer(c, result); drive(c); /* most answers fit the socket buffer at once */ } @@ -1837,13 +1902,15 @@ static void answer_handed(Host *host) { /* The owner's newest snapshot, held, shown by this thread's reader; NULL * when there is none. Taken for each round, after the round's answers from * the owner: those were handed back after their saves' snapshots. */ -static PureSnapshot *read_published(Host *host) { - Executor *ex = &host->owner->executor; +static PureSnapshot *read_published(Host *host, App *app) { + Executor *ex = &app->owner.executor; pthread_mutex_lock(&ex->published_lock); PureSnapshot *snapshot = ex->published; if (snapshot) pure_snapshot_hold(snapshot); pthread_mutex_unlock(&ex->published_lock); - if (snapshot) pure_app_read(&host->reader, snapshot); + PureApp *reader = &host->readers[app - apps]; + if (snapshot) pure_app_read(reader, snapshot); + reader->work_budget = app->work; return snapshot; } @@ -1855,7 +1922,7 @@ static PureSnapshot *read_published(Host *host) { * holds up, the owner's writes. */ enum { QUERIES = 64 }; typedef struct { PureModule module; unsigned refs; } Held; -static struct { char name[65]; Held *held; } queries[QUERIES]; +static struct { const App *app; char name[65]; Held *held; } queries[QUERIES]; static pthread_mutex_t queries_lock = PTHREAD_MUTEX_INITIALIZER; static void let_go_query(Held *held) { @@ -1889,7 +1956,8 @@ static bool same_states(const PureModule *a, const PureModule *b) { return true; } -static PureResult register_query(const Host *host, const Job *job) { +static PureResult register_query(const Job *job) { + const App *app = job->route.app; Held *held = calloc(1, sizeof(*held)); char error[128]; if (!held) return PURE_ALLOCATION; @@ -1897,7 +1965,7 @@ static PureResult register_query(const Host *host, const Job *job) { free(held); return PURE_INVALID; } - PureResult status = !same_states(&held->module, host->module) ? PURE_CONFLICT + PureResult status = !same_states(&held->module, &app->module) ? PURE_CONFLICT : pure_module_view(&held->module, job->effect.key) < 0 ? PURE_NOT_FOUND : PURE_OK; Held *old = NULL; @@ -1906,12 +1974,13 @@ static PureResult register_query(const Host *host, const Job *job) { pthread_mutex_lock(&queries_lock); int slot = -1; for (int i = 0; i < QUERIES && slot < 0; ++i) - if (queries[i].held && !strcmp(queries[i].name, job->effect.key)) slot = i; + if (queries[i].held && queries[i].app == app && !strcmp(queries[i].name, job->effect.key)) slot = i; for (int i = 0; i < QUERIES && slot < 0; ++i) if (!queries[i].held) slot = i; if (slot < 0) status = PURE_LIMIT; else { old = queries[slot].held; + queries[slot].app = app; snprintf(queries[slot].name, sizeof(queries[slot].name), "%s", job->effect.key); queries[slot].held = held; } @@ -1926,7 +1995,8 @@ static PureResult run_query(const Job *job, PureSnapshot *snapshot, Applied *a) Held *held = NULL; pthread_mutex_lock(&queries_lock); for (int i = 0; i < QUERIES && !held; ++i) - if (queries[i].held && !strcmp(queries[i].name, job->effect.key)) (held = queries[i].held)->refs++; + if (queries[i].held && queries[i].app == job->route.app && !strcmp(queries[i].name, job->effect.key)) + (held = queries[i].held)->refs++; pthread_mutex_unlock(&queries_lock); if (!held) return PURE_NOT_FOUND; if (!snapshot) { @@ -1935,7 +2005,7 @@ static PureResult run_query(const Job *job, PureSnapshot *snapshot, Applied *a) } PureApp reader = {0}; pure_app_read(&reader, snapshot); - pure_app_work(&reader, work_budget); + pure_app_work(&reader, job->route.app->work); reader.module = &held->module; /* the app's states, the query's views */ Route route = job->route; route.target = (uint32_t)pure_module_view(&held->module, job->effect.key); @@ -1945,6 +2015,12 @@ static PureResult run_query(const Job *job, PureSnapshot *snapshot, Applied *a) return status; } +/* Whether a job of the round is shown on this thread: a query, or a view + * whose app has a snapshot to show it from. */ +static bool shown_here(const Host *host, const Job *job) { + return job->route.kind == ROUTE_QUERY || (job->route.kind == ROUTE_VIEW && host->snapshots[job->route.app - apps]); +} + /* Every request made ready since the last round: run here, or handed to the * owner. Views are shown here from the snapshot when there is one, after the * rest went to the owner, which meanwhile applies and saves them. Answering @@ -1956,47 +2032,72 @@ static void serve_bursts(Host *host) { while (pure_runtime_take(&host->runtime, &effect) == PURE_TAKE_EFFECT) host->jobs[count++] = (Job){host, effect, host->routes[effect.ticket.slot], NULL, NULL}; if (!count) return; - if (!host->owner) { + if (!apps[0].owned) { /* the one app of the host, of documents alone: run here */ execute(&host->local, host->jobs, count, answer_burst, NULL); continue; } - PureSnapshot *snapshot = NULL; - size_t views = 0, queried = 0; + /* Each app with a view or a query in the round: its snapshot, read + * once (NULL: none yet, and its owner shows its views). */ + ++host->round; + size_t touched = 0; for (size_t i = 0; i < count; ++i) { - views += host->jobs[i].route.kind == ROUTE_VIEW; - queried += host->jobs[i].route.kind == ROUTE_QUERY; + const Route *route = &host->jobs[i].route; + unsigned a = (unsigned)(route->app - apps); + if ((route->kind != ROUTE_VIEW && route->kind != ROUTE_QUERY) || host->read_in[a] == host->round) continue; + host->read_in[a] = host->round; + host->snapshots[a] = read_published(host, route->app); + host->touched[touched++] = a; } - if ((views || queried) && !(snapshot = read_published(host))) views = 0; /* the owner shows them */ - size_t handed = count - views - queried; /* queries are all this thread's */ - if (handed) { - Owner *owner = host->owner; + /* The rest go to their apps' owners, an owner's all at once; past an + * app's queue budget they are refused (503 busy). */ + if (!grow(&host->views, &host->views_capacity, count, sizeof(*host->views))) abort(); + size_t answers = 0; + for (size_t i = 0; i < count; ++i) { + const Job *job = &host->jobs[i]; + App *app = job->route.app; + unsigned a = (unsigned)(app - apps); + if (shown_here(host, job) || host->handed_in[a] == host->round) continue; + host->handed_in[a] = host->round; + Owner *owner = &app->owner; + size_t handed = 0; pthread_mutex_lock(&owner->lock); - if (!grow(&owner->jobs, &owner->capacity, owner->count + handed, sizeof(*owner->jobs))) abort(); - for (size_t i = 0; i < count; ++i) - if ((!snapshot || host->jobs[i].route.kind != ROUTE_VIEW) && host->jobs[i].route.kind != ROUTE_QUERY) - owner->jobs[owner->count++] = host->jobs[i]; + for (size_t k = i; k < count; ++k) { + const Job *mine = &host->jobs[k]; + if (mine->route.app != app || shown_here(host, mine)) continue; + if (owner->queue && owner->count >= owner->queue) { + host->views[answers++] = (Applied){.host = host, .ticket = mine->effect.ticket, .input = mine->effect.input, + .route = mine->route.kind, .status = PURE_BUSY, .refused = "busy"}; + ++owner->busy; + continue; + } + if (!grow(&owner->jobs, &owner->capacity, owner->count + 1, sizeof(*owner->jobs))) abort(); + owner->jobs[owner->count++] = *mine; + ++handed; + } owner->arrivals += handed; - pthread_cond_signal(&owner->waiting); + pthread_cond_signal(&owner->waiting); /* it may be gathering */ pthread_mutex_unlock(&owner->lock); + if (handed) schedule(owner); host->pending += handed; } - if (!views && !queried) return; /* Shown first and answered after, as a burst is. */ - if (!grow(&host->views, &host->views_capacity, views + queried, sizeof(*host->views))) abort(); size_t shown = 0; for (size_t i = 0; i < count; ++i) { const Job *job = &host->jobs[i]; - if (job->route.kind != ROUTE_QUERY && (job->route.kind != ROUTE_VIEW || !snapshot)) continue; - Applied *a = &host->views[shown++]; + if (!shown_here(host, job)) continue; + unsigned a = (unsigned)(job->route.app - apps); + Applied *applied = &host->views[answers++]; bool registering = job->route.kind == ROUTE_QUERY && job->effect.input == PURE_INPUT_WRITE; - *a = (Applied){.host = host, .ticket = job->effect.ticket, .input = job->effect.input, - .route = registering ? ROUTE_QUERY : ROUTE_VIEW}; - a->status = job->route.kind == ROUTE_VIEW ? show_view(&host->reader, &job->route, job->effect.available, a) - : registering ? register_query(host, job) - : run_query(job, snapshot, a); + *applied = (Applied){.host = host, .ticket = job->effect.ticket, .input = job->effect.input, + .route = registering ? ROUTE_QUERY : ROUTE_VIEW}; + applied->status = job->route.kind == ROUTE_VIEW ? show_view(&host->readers[a], &job->route, job->effect.available, applied) + : registering ? register_query(job) + : run_query(job, host->snapshots[a], applied); + ++shown; } - pure_snapshot_release(snapshot); /* what answers lend holds its own mapping */ - for (size_t i = 0; i < shown; ++i) answer_applied(host, &host->views[i]); + for (size_t i = 0; i < touched; ++i) pure_snapshot_release(host->snapshots[host->touched[i]]); /* answers lend their own */ + if (!answers) return; + for (size_t i = 0; i < answers; ++i) answer_applied(host, &host->views[i]); host->shown += shown; } } @@ -2014,84 +2115,161 @@ static bool before(struct timespec a, struct timespec b) { return a.tv_sec < b.tv_sec || (a.tv_sec == b.tv_sec && a.tv_nsec < b.tv_nsec); } -static void *run_owner(void *context) { - Owner *owner = context; - Job *spare = NULL; - size_t spare_capacity = 0; - bool gather = false; - uint64_t answered_at = 0; /* arrivals when the last round's answers were handed back */ - for (;;) { - pthread_mutex_lock(&owner->lock); - /* Wait for requests, or until the next timer is due; trim what the - * wires delivered meanwhile. */ - for (;;) { - bool delivered = owner->delivered; - owner->delivered = false; - pthread_mutex_unlock(&owner->lock); - long due = run_timers(&owner->executor); - bool answered = (delivered || owner->executor.answers_wait) && take_deliveries(&owner->executor); - if (owner->executor.answers_wait && (due < 0 || due > ANSWER_WAIT_MS)) due = ANSWER_WAIT_MS; - pthread_mutex_lock(&owner->lock); - /* Answers are saved with the next round, or now if none waits. */ - if (answered && !owner->count && !owner->stopping) { - pthread_mutex_unlock(&owner->lock); - if (pure_app_save(&owner->executor.app) == PURE_OK) after_save(&owner->executor); - pthread_mutex_lock(&owner->lock); - } - if (owner->count || owner->stopping) break; - if (owner->delivered) continue; - if (due < 0) { - pthread_cond_wait(&owner->waiting, &owner->lock); - continue; - } - struct timespec wake = pure_wait_until(due * 1000000LL); - pthread_cond_timedwait(&owner->waiting, &owner->lock, &wake); - } - /* After a round that saved, its answers bring requests back from the - * connection threads: gather them until none has come for 100 µs (a - * millisecond at most), so they share one save as they would on a - * single thread, where answering takes that long anyway. The quiet - * counts only once a request came after the answers: requests that - * waited out the save alone would otherwise start the next round - * without the clients just answered, and the clients would settle - * into two halves taking turns, each save carrying half of them. - * Gathering never outlasts the time the last save waited for its - * device, nor does the quiet: waiting pays only while it costs less - * than the wait a save of the late requests would take on its own - * (a sync of milliseconds on a flash card; in RAM next to none, and - * there gathering would only leave the owner idle). */ - uint64_t waited = owner->executor.save_waited, budget = waited < 1000000 ? waited : 1000000; - struct timespec now, end; - clock_gettime(PURE_WAIT_CLOCK, &now); - end = later(now, (long)budget); - while (gather && owner->count && !owner->stopping && before(now, end)) { - size_t seen = owner->count; - bool returned = owner->arrivals != answered_at; - struct timespec quiet = later(now, budget < 100000 ? (long)budget : 100000); - pthread_cond_timedwait(&owner->waiting, &owner->lock, returned && before(quiet, end) ? &quiet : &end); - if (returned && owner->count == seen) break; - clock_gettime(PURE_WAIT_CLOCK, &now); - } - if (!owner->count) { - pthread_mutex_unlock(&owner->lock); - break; - } - Job *jobs = owner->jobs; - size_t count = owner->count, capacity = owner->capacity; - owner->jobs = spare; - owner->capacity = spare_capacity; - owner->count = 0; +/* The workers' queue of apps with work to do (docs/design/017): an app + * takes its turns in order, on one worker at a time. */ +static struct { + pthread_mutex_t lock; + pthread_cond_t wake; /* a worker waits for an app, or for the soonest timer due (on the monotonic clock) */ + Owner *head, *tail; + bool stopping; +} turns = {.lock = PTHREAD_MUTEX_INITIALIZER}; + +static void enqueue(Owner *owner) { + owner->queued = true; + owner->next = NULL; + if (turns.tail) turns.tail->next = owner; + else turns.head = owner; + turns.tail = owner; + pthread_cond_signal(&turns.wake); +} + +/* Work came for an app: a turn, unless it waits for one or has one (then + * another after it). Any thread, holding none of the owner's locks. */ +static void schedule(Owner *owner) { + pthread_mutex_lock(&turns.lock); + if (owner->running) owner->again = true; + else if (!owner->queued) enqueue(owner); + pthread_mutex_unlock(&turns.lock); +} + +/* The most requests a turn takes when other apps wait for a worker (all + * of them when none does): a connection thread's round. */ +enum { TURN = BATCH }; + +/* An app's turn: its timers due, what its wires delivered, and the + * requests waiting for it, applied and saved once, their answers handed + * back. Alone (no other app waits for a worker), it first gathers the + * clients its last save answered. Whether it has requests left at once; + * due, its next timer's milliseconds (-1: none). */ +static bool owner_turn(Owner *owner, bool alone, long *due) { + Executor *ex = &owner->executor; + pthread_mutex_lock(&owner->lock); + bool delivered = owner->delivered; + owner->delivered = false; + pthread_mutex_unlock(&owner->lock); + *due = run_timers(ex); + bool answered = (delivered || ex->answers_wait) && take_deliveries(ex); + if (ex->answers_wait && (*due < 0 || *due > ANSWER_WAIT_MS)) *due = ANSWER_WAIT_MS; + pthread_mutex_lock(&owner->lock); + /* Answers are saved with the requests waiting, or now if none waits. */ + if (answered && !owner->count) { pthread_mutex_unlock(&owner->lock); - owner->executor.wrote = false; - execute(&owner->executor, jobs, count, hand_back, owner); - gather = owner->executor.wrote; + if (pure_app_save(&ex->app) == PURE_OK) after_save(ex); pthread_mutex_lock(&owner->lock); - answered_at = owner->arrivals; + } + /* After a turn that saved, its answers bring requests back from the + * connection threads: gather them until none has come for 100 µs (a + * millisecond at most), so they share one save as they would on a + * single thread, where answering takes that long anyway. The quiet + * counts only once a request came after the answers: requests that + * waited out the save alone would otherwise start the next turn + * without the clients just answered, and the clients would settle + * into two halves taking turns, each save carrying half of them. + * Gathering never outlasts the time the last save waited for its + * device, nor does the quiet: waiting pays only while it costs less + * than the wait a save of the late requests would take on its own (a + * sync of milliseconds on a flash card; in RAM next to none, and there + * gathering would only leave the worker idle). It gathers only when no + * other app waits for the worker. */ + uint64_t waited = ex->save_waited, budget = waited < 1000000 ? waited : 1000000; + struct timespec now, end; + clock_gettime(PURE_WAIT_CLOCK, &now); + end = later(now, (long)budget); + while (alone && owner->gather && owner->count && !owner->stopping && before(now, end)) { + size_t seen = owner->count; + bool returned = owner->arrivals != owner->answered_at; + struct timespec quiet = later(now, budget < 100000 ? (long)budget : 100000); + pthread_cond_timedwait(&owner->waiting, &owner->lock, returned && before(quiet, end) ? &quiet : &end); + if (returned && owner->count == seen) break; + clock_gettime(PURE_WAIT_CLOCK, &now); + } + size_t count = owner->count, take = alone || count <= TURN ? count : TURN; + if (!take) { pthread_mutex_unlock(&owner->lock); - spare = jobs; - spare_capacity = capacity; + return false; } - free(spare); + Job *jobs = owner->jobs; + size_t capacity = owner->capacity; + if (take == count) { /* all of them: the arrays trade places */ + owner->jobs = owner->spare; + owner->capacity = owner->spare_capacity; + owner->count = 0; + } else { /* a turn's worth into the spare array; the rest wait for the next */ + if (!grow(&owner->spare, &owner->spare_capacity, take, sizeof(*owner->spare))) abort(); + memcpy(owner->spare, jobs, take * sizeof(*jobs)); + memmove(jobs, jobs + take, (count - take) * sizeof(*jobs)); + owner->count = count - take; + jobs = owner->spare; + capacity = owner->spare_capacity; + } + owner->spare = NULL; /* it is the turn's now, until it is over */ + owner->spare_capacity = 0; + pthread_mutex_unlock(&owner->lock); + ex->wrote = false; + execute(ex, jobs, take, hand_back, owner); + owner->gather = ex->wrote; + pthread_mutex_lock(&owner->lock); + owner->answered_at = owner->arrivals; + owner->spare = jobs; + owner->spare_capacity = capacity; + bool more = owner->count > 0; + pthread_mutex_unlock(&owner->lock); + return more; +} + +/* A worker: the next app in the queue takes its turn; an app whose timer + * is due joins the queue; with none, it sleeps until the soonest is due. + * It stops once the host stops and no app waits. */ +static void *run_worker(void *context) { + (void)context; + pthread_mutex_lock(&turns.lock); + for (;;) { + uint64_t now = now_ms(), soonest = UINT64_MAX; + for (unsigned i = 0; i < app_count; ++i) { + Owner *o = &apps[i].owner; + if (!apps[i].running || !apps[i].owned || !o->due || o->queued || o->running) continue; + if (o->due <= now) { + o->due = 0; + enqueue(o); + } else if (o->due < soonest) soonest = o->due; + } + Owner *owner = turns.head; + if (!owner) { + if (turns.stopping) break; + if (soonest == UINT64_MAX) pthread_cond_wait(&turns.wake, &turns.lock); + else { + struct timespec wake = pure_wait_until((long long)(soonest - now) * 1000000LL); + pthread_cond_timedwait(&turns.wake, &turns.lock, &wake); + } + continue; + } + turns.head = owner->next; + if (!turns.head) turns.tail = NULL; + owner->queued = false; + owner->running = true; + owner->again = false; + bool alone = !turns.head; + pthread_mutex_unlock(&turns.lock); + long due; + bool more = owner_turn(owner, alone, &due); + pthread_mutex_lock(&turns.lock); + owner->running = false; + ++owner->turns; + owner->due = due < 0 ? 0 : now_ms() + (uint64_t)due; + if (more || owner->again) enqueue(owner); + } + pthread_cond_broadcast(&turns.wake); /* the other workers stop too */ + pthread_mutex_unlock(&turns.lock); return NULL; } @@ -2134,6 +2312,7 @@ static PureResult surface_event(PureSurfaceHost *s, uint32_t input, PureSpan tex pthread_cond_signal(&owner->waiting); } pthread_mutex_unlock(&owner->lock); + if (queued) schedule(owner); pthread_mutex_lock(&s->lock); while (queued && !waiter.done) pthread_cond_wait(&s->applied, &s->lock); pthread_mutex_unlock(&s->lock); @@ -2206,10 +2385,14 @@ static bool replies_borrow(const PureModule *m, uint32_t input) { /* ---- Main ------------------------------------------------------------- */ -static bool load_module(PureModule *m, const char *path, bool *documents, uint32_t *show_out, uint32_t *edit_out) { +/* An app's module from its file, and whether it serves documents; false, + * with why, when it does not load or cannot be served. */ +static bool load_module(App *app, const char *path) { + PureModule *m = &app->module; + bool *documents = &app->documents; char error[160]; if (pure_module_read(path, m, error, sizeof(error)) != PURE_OK) { - fprintf(stderr, "Module: %s\n", error); + snprintf(app->why, sizeof(app->why), "Module: %s", error); return false; } /* Documents are served when the module keeps a keyed collection: its keyed @@ -2219,18 +2402,18 @@ static bool load_module(PureModule *m, const char *path, bool *documents, uint32 *documents = m->collections > 0; if (*documents && (show < 0 || edit < 0 || m->input[show].kind != PURE_KIND_MESSAGE || !m->input[show].keyed || m->input[edit].kind != PURE_KIND_MESSAGE || !m->input[edit].keyed)) { - fputs("Module: documents need keyed messages named show and edit\n", stderr); + snprintf(app->why, sizeof(app->why), "Module: documents need keyed messages named show and edit"); return false; } for (uint32_t i = 0; i < m->inputs; ++i) { bool served = (*documents && i == (uint32_t)edit) || (m->input[i].kind == PURE_KIND_MESSAGE && !m->input[i].keyed); if (served && !replies_borrow(m, i)) { - fprintf(stderr, "Module: %s may reply only a field of its message\n", m->input_names[i]); + snprintf(app->why, sizeof(app->why), "Module: %s may reply only a field of its message", m->input_names[i]); return false; } } - *show_out = (uint32_t)(show >= 0 ? show : 0); - *edit_out = (uint32_t)(edit >= 0 ? edit : 0); + app->show = (uint32_t)(show >= 0 ? show : 0); + app->edit = (uint32_t)(edit >= 0 ? edit : 0); return true; } @@ -2274,18 +2457,26 @@ static void accept_all(Host *host) { } -static bool start_executor(Executor *ex, const PureModule *module, uint32_t show, uint32_t edit, const char *store) { - ex->module = module; - ex->show = show; - ex->edit = edit; - if (pure_store_open(store, &ex->store) != PURE_OK) { fputs("Cannot open the store\n", stderr); return false; } - if (pure_app_start(&ex->app, module, pure_store_data(ex->store)) != PURE_OK || !pure_app_defer(&ex->app, true)) { - fprintf(stderr, "Cannot start the module on the store%s%s\n", ex->app.why[0] ? ": " : "", ex->app.why); +/* An app's module on its store, with its budgets; false (why says why) + * when the store does not open or the module does not start on it. */ +static bool start_executor(Executor *ex, App *app) { + ex->module = &app->module; + ex->show = app->show; + ex->edit = app->edit; + if (pure_store_open(app->store, &ex->store) != PURE_OK) { + snprintf(app->why, sizeof(app->why), "Cannot open the store"); + return false; + } + if (pure_app_start(&ex->app, &app->module, pure_store_data(ex->store)) != PURE_OK || !pure_app_defer(&ex->app, true)) { + snprintf(app->why, sizeof(app->why), "Cannot start the module on the store%s%s", ex->app.why[0] ? ": " : "", + ex->app.why); + pure_store_close(ex->store); + ex->store = NULL; return false; } pure_app_background_checkpoints(&ex->app, true); - pure_app_memory(&ex->app, memory_budget); - pure_app_work(&ex->app, work_budget); + pure_app_memory(&ex->app, app->memory); + pure_app_work(&ex->app, app->work); return true; } @@ -2300,13 +2491,18 @@ static void stop_executor(Executor *ex) { /* One connection thread: its own listener on the shared port, epoll set, * runtime domain and, unless an owner runs the app for all of them, app and * store handle. */ -static bool start_shard(Host *host, const PureModule *module, bool documents, uint32_t show, uint32_t edit, - const char *store, size_t budget, unsigned *port) { - host->module = module; - host->documents = documents; +static bool start_shard(Host *host, size_t budget, unsigned *port) { host->wake = -1; - pure_app_work(&host->reader, work_budget); /* the views it shows from snapshots */ - if (!host->owner && !start_executor(&host->local, module, show, edit, store)) return false; + if (!apps[0].owned && !start_executor(&host->local, &apps[0])) { + fprintf(stderr, "%s\n", apps[0].why); + return false; + } + host->readers = calloc(app_count, sizeof(*host->readers)); + host->snapshots = calloc(app_count, sizeof(*host->snapshots)); + host->read_in = calloc(app_count, sizeof(*host->read_in)); + host->handed_in = calloc(app_count, sizeof(*host->handed_in)); + host->touched = calloc(app_count, sizeof(*host->touched)); + if (!host->readers || !host->snapshots || !host->read_in || !host->handed_in || !host->touched) return false; host->work = calloc(host->capacity, sizeof(*host->work)); host->table = calloc(host->capacity, sizeof(*host->table)); host->by_slot = calloc(host->capacity, sizeof(*host->by_slot)); @@ -2339,8 +2535,8 @@ static bool start_shard(Host *host, const PureModule *module, bool documents, ui perror("epoll"); return false; } - /* The owner says it handed requests back through an eventfd. */ - if (host->owner) { + /* The owners say they handed requests back through an eventfd. */ + if (apps[0].owned) { struct epoll_event wake_event = {.events = EPOLLIN, .data.ptr = &host->wake}; if (pthread_mutex_init(&host->lock, NULL) || (host->wake = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC)) < 0 || epoll_ctl(host->epoll, EPOLL_CTL_ADD, host->wake, &wake_event)) { @@ -2358,7 +2554,7 @@ static void *run_shard(void *context) { uint64_t next_sweep = host->now + 1000; while (!atomic_load(&stopping)) { /* A thread that runs the app ticks its timers on time. */ - long due = host->owner ? -1 : run_timers(&host->local); + long due = apps[0].owned ? -1 : run_timers(&host->local); int ready = epoll_wait(host->epoll, events, EVENTS, due >= 0 && due < 1000 ? (int)due : 1000); host->now = now_ms(); for (int e = 0; e < ready; ++e) { @@ -2401,8 +2597,13 @@ static void *run_shard(void *context) { } static void stop_shard(Host *host) { - if (!host->owner) stop_executor(&host->local); - pure_app_stop(&host->reader); + if (!apps[0].owned) stop_executor(&host->local); + for (unsigned i = 0; host->readers && i < app_count; ++i) pure_app_stop(&host->readers[i]); + free(host->readers); + free(host->snapshots); + free(host->read_in); + free(host->handed_in); + free(host->touched); if (host->listener >= 0) close(host->listener); if (host->epoll >= 0) close(host->epoll); if (host->wake >= 0) close(host->wake); @@ -2473,7 +2674,7 @@ static bool link_deliver(void *context, const PureLinkDelivery *d) { if (!queued) { free(ingress); free(link); - } + } else schedule(owner); return queued; } @@ -2484,6 +2685,7 @@ static void wire_delivered(void *context) { owner->delivered = true; pthread_cond_signal(&owner->waiting); pthread_mutex_unlock(&owner->lock); + schedule(owner); } /* --send OUTPUT=URL: that output's saved sends go to url, from NAME.OUTPUT. */ @@ -2649,17 +2851,112 @@ static bool wire_replies(Executor *ex, const PureModule *m, const char *const *r /* --threads auto: a connection thread for each CPU this process may run on, * less the one the owner of an app with states takes. */ -static unsigned auto_threads(bool owner) { +/* An app's owner, its executor started on the app's store, its snapshot + * published; false (why says why) when the app cannot start. */ +static bool start_owner(App *app, Host *hosts, unsigned threads) { + Owner *owner = &app->owner; + if (!start_executor(&owner->executor, app)) return false; + if (pthread_mutex_init(&owner->lock, NULL) || pure_cond_monotonic(&owner->waiting) || + pthread_mutex_init(&owner->executor.published_lock, NULL)) { + snprintf(app->why, sizeof(app->why), "Cannot make the owner's locks"); + return false; + } + owner->executor.reads_first = true; + owner->executor.publishes = true; + owner->hosts = hosts; + owner->host_count = 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) { + FILE *file = fopen(path, "r"); + if (!file) { + fprintf(stderr, "--apps %s: %s\n", path, strerror(errno)); + return false; + } + char line[4096]; + unsigned at = 0; + bool ok = true; + while (ok && fgets(line, sizeof(line), file)) { + ++at; + char *hash = strchr(line, '#'), *words[8], *rest = NULL; + if (hash) *hash = 0; + unsigned count = 0; + for (char *word = strtok_r(line, " \t\r\n", &rest); word && count < 8; word = strtok_r(NULL, " \t\r\n", &rest)) + words[count++] = word; + if (!count) continue; + App *grown = count >= 3 && count <= 6 ? realloc(apps, (app_count + 1) * sizeof(*apps)) : NULL; + if (!grown) { + fprintf(stderr, "--apps %s:%u: NAME MODULE STORE [memory=BYTES] [work=N] [queue=N]\n", path, at); + ok = false; + break; + } + apps = grown; + App *app = &apps[app_count]; + memset(app, 0, sizeof(*app)); + 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; + 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); + ok = false; + break; + } + memcpy(app->name, words[0], length + 1); + for (unsigned w = 3; ok && w < count; ++w) { + char *value = strchr(words[w], '='); + uint64_t n; + 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 { + fprintf(stderr, "--apps %s:%u: budgets are memory=BYTES, work=N and queue=N\n", 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->module = (PureModule){0}; /* stopped, with why */ + else if (ok) app->running = true; /* until its store says otherwise */ + } + fclose(file); + if (ok && !app_count) fprintf(stderr, "--apps %s: no app\n", path); + return ok && app_count; +} + +/* A JSON string's quotes and escapes around text. */ +static void print_json(FILE *out, const char *text) { + fputc('"', out); + for (const unsigned char *c = (const unsigned char *)text; *c; ++c) + if (*c == '"' || *c == '\\') fprintf(out, "\\%c", *c); + else if (*c < 0x20) fprintf(out, "\\u%04x", *c); + else fputc(*c, out); + fputc('"', out); +} + +static unsigned online_cpus(void) { cpu_set_t set; long cpus = sched_getaffinity(0, sizeof(set), &set) ? sysconf(_SC_NPROCESSORS_ONLN) : CPU_COUNT(&set); - if (cpus < 1) cpus = 1; - if (owner && cpus > 1) --cpus; - return cpus > 256 ? 256 : (unsigned)cpus; + return cpus < 1 ? 1 : cpus > 256 ? 256 : (unsigned)cpus; +} + +static unsigned auto_threads(bool owner) { + unsigned cpus = online_cpus(); + return owner && cpus > 1 ? cpus - 1 : cpus; } int main(int argc, char **argv) { - static PureModule module; - static Owner owner; static PureSurfaceHost surface; /* What follows -- is the surface's: its library first, then its arguments. */ int options = argc, surface_argc = 0; @@ -2670,9 +2967,9 @@ int main(int argc, char **argv) { break; } const char *surface_path = NULL, *module_path = NULL, *store = NULL, *name = NULL, *sends[PURE_MODULE_INPUTS], *replies[PURE_MODULE_INPUTS]; - const char *seals[PURE_MODULE_INPUTS], *named[PURE_MODULE_INPUTS], *key_path = NULL, *senders_path = NULL; + const char *seals[PURE_MODULE_INPUTS], *named[PURE_MODULE_INPUTS], *key_path = NULL, *senders_path = NULL, *apps_path = NULL; unsigned send_count = 0, reply_count = 0, seal_count = 0, named_count = 0; - unsigned port = 8767, threads = 1, link_port = 0; + unsigned port = 8767, threads = 1, link_port = 0, workers = 0; size_t budget = 64u << 20, capacity = 1024; uint64_t timeout_ms = 10000, link_idle_ms = 0; for (int i = 1; i < options; i += 2) { @@ -2680,6 +2977,7 @@ int main(int argc, char **argv) { if (!strcmp(argv[i], "--sealed-only")) { sealed_only = true; --i; continue; } if (i + 1 == options) goto usage; if (!strcmp(argv[i], "--module")) module_path = argv[i + 1]; + else if (!strcmp(argv[i], "--apps")) apps_path = argv[i + 1]; else if (!strcmp(argv[i], "--surface")) surface_path = argv[i + 1]; else if (!strcmp(argv[i], "--key")) key_path = argv[i + 1]; else if (!strcmp(argv[i], "--seal") && seal_count < PURE_MODULE_INPUTS) seals[seal_count++] = argv[i + 1]; @@ -2700,6 +2998,7 @@ int main(int argc, char **argv) { else if (!strcmp(argv[i], "--connections") && number(argv[i + 1], 1u << 20, &value) && value) capacity = (size_t)value; else if (!strcmp(argv[i], "--threads") && !strcmp(argv[i + 1], "auto")) threads = 0; else if (!strcmp(argv[i], "--threads") && number(argv[i + 1], 256, &value) && value) threads = (unsigned)value; + else if (!strcmp(argv[i], "--workers") && number(argv[i + 1], 256, &value) && value) workers = (unsigned)value; else if (!strcmp(argv[i], "--timeout-ms") && number(argv[i + 1], 3600000, &value) && value) timeout_ms = value; else goto usage; } @@ -2707,10 +3006,33 @@ int main(int argc, char **argv) { for (int i = options + 1; i < argc && surface_argc < 63; ++i) surface_argv[surface_argc++] = argv[i]; surface_argv[surface_argc] = NULL; if (options < argc && !surface_path) goto usage; - uint32_t show, edit; - bool documents; - if (!module_path || !store || !load_module(&module, module_path, &documents, &show, &edit)) goto usage; - if (!threads) threads = auto_threads(module.states); + if (apps_path) { /* a host of apps: each its own module, store and budgets; none sends yet (017, stage 1) */ + if (module_path || store || name || send_count || reply_count || seal_count || link_port || surface_path || + memory_budget || work_budget) { + fputs("--apps gives each app its module, store and budgets; --send, --reply, --seal, --link-port, --name " + "and --surface are for a host of one app (--module)\n", stderr); + return 2; + } + if (!read_apps(apps_path)) return 2; + named_apps = true; + } else { + if (!module_path || !store || !(apps = calloc(1, sizeof(*apps)))) goto usage; + app_count = 1; + apps[0].store = store; + apps[0].memory = memory_budget; + apps[0].work = work_budget; + if (!load_module(&apps[0], module_path)) { + fprintf(stderr, "%s\n", apps[0].why); + goto usage; + } + /* States (and outputs, whose sends are the app's state) have one + * owner, whose snapshots the connection threads read. */ + bool outputs = false; + for (uint32_t i = 0; i < apps[0].module.inputs; ++i) outputs = outputs || apps[0].module.input[i].kind == PURE_KIND_OUTPUT; + apps[0].owned = apps[0].module.states || outputs; + apps[0].running = true; + } + if (!threads) threads = auto_threads(apps[0].owned); char public_key[2 * PURE_SEAL_KEY + 1] = ""; if (key_path) { char error[256]; @@ -2719,68 +3041,92 @@ int main(int argc, char **argv) { if ((sealed_only || named_count) && !seal_key) { fputs("--sealed-only and --sender need --key\n", stderr); return 2; } if (!name_senders(named, named_count)) return 2; if (seal_key && !pure_seal_peer(public_key, own_public)) return 2; - if (pure_store_init(store) != PURE_OK) { fputs("Cannot initialize the store directory\n", stderr); return 1; } signal(SIGPIPE, SIG_IGN); struct sigaction stop_action = {.sa_handler = stop}; sigemptyset(&stop_action.sa_mask); sigaction(SIGINT, &stop_action, NULL); sigaction(SIGTERM, &stop_action, NULL); Host *hosts = calloc(threads, sizeof(*hosts)); - pthread_t *thread = calloc(threads, sizeof(*thread)), owner_thread; - if (!hosts || !thread) return 1; - /* States (and outputs, whose sends are the app's state) have one owner on - * a thread of its own, whose snapshots the connection threads read. */ - bool outputs = false; - for (uint32_t i = 0; i < module.inputs; ++i) outputs = outputs || module.input[i].kind == PURE_KIND_OUTPUT; - bool owned = module.states || outputs; + pthread_t *thread = calloc(threads, sizeof(*thread)); + if (!hosts || !thread || pure_cond_monotonic(&turns.wake)) return 1; + /* 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) { + App *app = &apps[a]; + if (!app->running) continue; + struct stat mine, theirs; + bool shared = false; + 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"); + else + for (unsigned b = 0; b < a && !shared; ++b) + if (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); + shared = true; + } + app->running = !app->why[0] && (!app->owned || start_owner(app, hosts, threads)); + if (!app->running && !named_apps) { + 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]; - if (!name) { + if (!named_apps && !name) { const char *base = strrchr(module_path, '/') ? strrchr(module_path, '/') + 1 : module_path; size_t length = strlen(base); if (length > 5 && !strcmp(base + length - 5, ".pmod")) length -= 5; snprintf(stem, sizeof(stem), "%.*s", (int)length, base); name = stem; } - if (owned) { - if (!start_executor(&owner.executor, &module, show, edit, store) || pthread_mutex_init(&owner.lock, NULL) || - pure_cond_monotonic(&owner.waiting) || pthread_mutex_init(&owner.executor.published_lock, NULL)) - return 1; - owner.executor.reads_first = true; - owner.executor.publishes = true; - if (!open_wires(&owner.executor, &module, sends, send_count, name) || - !wire_replies(&owner.executor, &module, replies, reply_count) || - !wire_seals(&owner.executor, &module, seals, seal_count, seal_key ? own_public : NULL)) return 2; - after_save(&owner.executor); /* the snapshot, and the sends stored before */ - for (unsigned i = 0; i < owner.executor.wire_count; ++i) - if (!pure_wire_start(owner.executor.wires[i], wire_delivered, &owner)) { fputs("Cannot start a wire\n", stderr); return 1; } - for (unsigned i = 0; i < owner.executor.link_count; ++i) { + Owner *owner = &apps[0].owner; /* the one app's, in a host of one */ + if (!named_apps && apps[0].owned && + (!open_wires(&owner->executor, &apps[0].module, sends, send_count, name) || + !wire_replies(&owner->executor, &apps[0].module, replies, reply_count) || + !wire_seals(&owner->executor, &apps[0].module, seals, seal_count, seal_key ? own_public : NULL))) return 2; + for (unsigned a = 0; a < app_count; ++a) + if (apps[a].running && apps[a].owned) after_save(&apps[a].owner.executor); /* the snapshot, and the sends stored before */ + if (!named_apps && apps[0].owned) { + for (unsigned i = 0; i < owner->executor.wire_count; ++i) + if (!pure_wire_start(owner->executor.wires[i], wire_delivered, owner)) { fputs("Cannot start a wire\n", stderr); return 1; } + for (unsigned i = 0; i < owner->executor.link_count; ++i) { char error[256]; - if (!pure_link_start(owner.executor.links[i], wire_delivered, &owner, error, sizeof(error))) { + if (!pure_link_start(owner->executor.links[i], wire_delivered, owner, error, sizeof(error))) { fprintf(stderr, "--send %s\n", error); return 2; } } - owner.hosts = hosts; - owner.host_count = threads; } for (unsigned i = 0; i < threads; ++i) { hosts[i].capacity = capacity; hosts[i].timeout_ms = timeout_ms; hosts[i].listener = hosts[i].epoll = -1; - hosts[i].owner = owned ? &owner : NULL; - if (!start_shard(&hosts[i], &module, documents, show, edit, store, budget, &port)) return 1; - } - if (owned && pthread_create(&owner_thread, NULL, run_owner, &owner)) { fputs("Cannot start the owner\n", stderr); return 1; } - if (owned) pthread_setname_np(owner_thread, "pure-owner"); /* its CPU, seen apart: one core at most */ + 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 */ if (link_port) { /* records from the senders named, over UDP */ char error[256]; - if (!owned || !seal_key) { + if (named_apps || !apps[0].owned || !seal_key) { fputs("--link-port needs an app with states and the node's --key\n", stderr); return 2; } PureLinkReceiver *receiver = - pure_link_receiver(seal_key, own_public, &(PureLinkHost){link_input, link_deliver, &owner}, error, sizeof(error)); + pure_link_receiver(seal_key, own_public, &(PureLinkHost){link_input, link_deliver, owner}, error, sizeof(error)); bool allowed = receiver != NULL && allow_senders(receiver, senders_path, error, sizeof(error)); for (unsigned i = 0; allowed && i < sender_count; ++i) allowed = pure_link_allow(receiver, senders[i].name, senders[i].key, error, sizeof(error)); @@ -2794,15 +3140,29 @@ int main(int argc, char **argv) { return 2; } } - /* Its name too: its sends' sources are NAME.OUTPUT, which a receiver's --sender must name. */ - printf("{\"port\":%u,\"threads\":%u,\"name\":\"", port, threads); - for (const char *c = name; *c; ++c) printf(*c == '"' || *c == '\\' ? "\\%c" : "%c", *c); - if (*public_key) printf("\",\"public_key\":\"%s\"}\n", public_key); - else printf("\"}\n"); + if (named_apps) { /* each app, and why one stopped */ + printf("{\"port\":%u,\"threads\":%u,\"workers\":%u,\"apps\":[", port, threads, owners ? workers : 0); + for (unsigned a = 0; a < app_count; ++a) { + printf("%s{\"name\":", a ? "," : ""); + print_json(stdout, apps[a].name); + printf(",\"running\":%s", apps[a].running ? "true" : "false"); + if (!apps[a].running) { + printf(",\"why\":"); + print_json(stdout, apps[a].why); + } + printf("}"); + } + printf("]%s%s%s}\n", *public_key ? ",\"public_key\":\"" : "", public_key, *public_key ? "\"" : ""); + } else { /* its name too: its sends' sources are NAME.OUTPUT, which a receiver's --sender must name */ + printf("{\"port\":%u,\"threads\":%u,\"name\":\"", port, threads); + for (const char *c = name; *c; ++c) printf(*c == '"' || *c == '\\' ? "\\%c" : "%c", *c); + if (*public_key) printf("\",\"public_key\":\"%s\"}\n", public_key); + else printf("\"}\n"); + } fflush(stdout); /* A surface reads the owner's snapshots and gives it events. */ - if (surface_path && !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; + 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; for (unsigned i = 1; i < threads; ++i) if (pthread_create(&thread[i], NULL, run_shard, &hosts[i])) { fputs("Cannot start a thread\n", stderr); return 1; } run_shard(&hosts[0]); @@ -2811,19 +3171,24 @@ int main(int argc, char **argv) { surface_changed(&surface); pthread_join(surface.thread, NULL); } - if (owned) { - pthread_mutex_lock(&owner.lock); - owner.stopping = true; - pthread_cond_signal(&owner.waiting); - pthread_mutex_unlock(&owner.lock); - pthread_join(owner_thread, NULL); - } + for (unsigned a = 0; a < app_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); + o->stopping = true; + pthread_cond_signal(&o->waiting); + pthread_mutex_unlock(&o->lock); + } + pthread_mutex_lock(&turns.lock); + 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); 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; - uint64_t accepted = 0, requests = 0, bursts = owner.executor.bursts, events = owner.executor.burst_events, expired = 0, - ticks = owner.executor.ticks, shown = 0, checkpoints = 0, memory_refused = owner.executor.memory_refused, - windows_full = owner.executor.windows_full; + size_t active = 0, retained = 0, state_bytes = 0; + uint64_t accepted = 0, requests = 0, bursts = 0, events = 0, expired = 0, ticks = 0, shown = 0, checkpoints = 0, + memory_refused = 0, windows_full = 0, publications = 0, answers = 0, delivered = 0, attempts = 0; for (unsigned i = 0; i < threads; ++i) { shown += hosts[i].shown; active += hosts[i].active; @@ -2837,28 +3202,36 @@ int main(int argc, char **argv) { expired += hosts[i].expired; stop_shard(&hosts[i]); } - uint64_t delivered = 0, attempts = 0; - for (unsigned i = 0; i < owner.executor.wire_count; ++i) { - uint64_t d, a; - pure_wire_counts(owner.executor.wires[i], &d, &a); - delivered += d; - attempts += a; - pure_wire_close(owner.executor.wires[i]); /* what was not delivered stays in the outbox */ - } - for (unsigned i = 0; i < owner.executor.link_count; ++i) { - PureLinkCounts c; - pure_link_counts(owner.executor.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(owner.executor.links[i]); /* what was not stored stays in the outbox */ - } - size_t state_bytes = owned ? pure_app_state_bytes(&owner.executor.app) : 0; - if (owned) { - checkpoints = owner.executor.checkpoints; - stop_executor(&owner.executor); - checkpoints = owner.executor.checkpoints; - free(owner.jobs); + 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 */ + } + state_bytes += pure_app_state_bytes(&ex->app); + stop_executor(ex); + checkpoints += ex->checkpoints; + free(apps[a].owner.jobs); + free(apps[a].owner.spare); } unsigned long groups, grouped; pure_store_group_counts(&groups, &grouped); @@ -2873,15 +3246,29 @@ int main(int argc, char **argv) { ",\"listener_deliveries\":%" PRIu64 ",\"listener_woken\":%" PRIu64 ",\"listener_remembered\":%" PRIu64 ",\"listener_swept\":%" PRIu64 ",\"listener_active\":%" PRIu64 ",\"listener_keyed\":%" PRIu64 ",\"listener_secrets\":%" PRIu64 ",\"listener_epochs\":%" PRIu64 ",\"listener_refused\":%" PRIu64 - ",\"state_bytes\":%zu,\"memory_refused\":%" PRIu64 ",\"windows_full\":%" PRIu64 "}\n", - active, retained, accepted, requests, bursts, events, ticks, expired, threads, owned ? "true" : "false", - owner.executor.publications, shown, delivered, attempts, owner.executor.answers, groups, grouped, + ",\"state_bytes\":%zu,\"memory_refused\":%" PRIu64 ",\"windows_full\":%" PRIu64, + active, retained, accepted, requests, bursts, events, ticks, expired, threads, apps[0].owned ? "true" : "false", + publications, shown, delivered, attempts, answers, groups, grouped, checkpoints, sent.packets, sent.records, sent.resent, sent.lost, sent.acknowledged, sent.stored, sent.refused, heard.packets, heard.received, heard.deliveries, heard.woken, heard.remembered, heard.swept, heard.active, heard.keyed, heard.secrets, heard.epochs, heard.refused, state_bytes, memory_refused, windows_full); + if (named_apps) { /* what each app did */ + fprintf(stderr, ",\"workers\":%u,\"apps\":[", owners ? workers : 0); + for (unsigned a = 0; a < app_count; ++a) { + const Owner *o = &apps[a].owner; + fprintf(stderr, "%s{\"name\":", a ? "," : ""); + print_json(stderr, apps[a].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); + } + fputc(']', stderr); + } + fputs("}\n", stderr); free(hosts); free(thread); + free(worker); return 0; usage: fputs("pure-serve --module FILE.pmod --store DIRECTORY [--port 8767] [--threads 1|N|auto] [--budget BYTES] " @@ -2889,6 +3276,11 @@ 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] " + "[--timeout-ms 10000]\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]; its routes start /NAME/\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" " --send OUTPUT=http[s]://host:port/path: to anything else, over HTTP\n" diff --git a/tests/check.py b/tests/check.py index 47ddf23..fdb4f56 100644 --- a/tests/check.py +++ b/tests/check.py @@ -3726,6 +3726,122 @@ def server_work(aup, pure, work): return first +APPS_SOURCE = """record Item { + n = 0 +} +state n = 0 +state items: list of Item +input add = message { step: number } +input fill = message { k: number } +input walk = message { step: number } +on add { + n = n + add.step +} +on fill { + items.append(Item { n: fill.k }) +} +on walk { + for i in items { + i.n = i.n + walk.step + } +} +view total = n +view count = count(i in items where i.n > 0) +""" + + +def server_apps(aup, pure, work): + """One host, many apps (docs/design/017): each app's routes start with its + name and reach its own states (two apps of one module share nothing); an + app whose module does not load, or whose store is another app's, stops + alone (503 stopped) while the others run; past an app's queue budget its + requests are refused (503 busy) and another app answers meanwhile; past + its work budget an event is refused (422 work) where the same event runs + in an app without one; an app whose store another process holds cannot + save (503 store_busy, nothing applied) while the others save; and after a + restart each app has its states back.""" + import fcntl + import socket + source = work / 'apps.aup' + source.write_text(APPS_SOURCE) + compile_module(aup, source, work / 'apps.pmod') + (work / 'broken.pmod').write_bytes(b'PUREMOD6 not a module') + manifest = work / 'apps.conf' + manifest.write_text(f"""# name module store budgets +a {work / 'apps.pmod'} {work / 'store-a'} +b {work / 'apps.pmod'} {work / 'store-b'} +busy {work / 'apps.pmod'} {work / 'store-busy'} queue=2 +tight {work / 'apps.pmod'} {work / 'store-tight'} work=55 +broken {work / 'broken.pmod'} {work / 'store-broken'} +twin {work / 'apps.pmod'} {work / 'store-a'} +""") + serve = pure / 'build/pure-serve' + + def get(port, path): + status, why, _, body = http(port, 'GET', path) + return status, why, body + + def post(port, path): + status, why, _, _ = http(port, 'POST', path) + return status, why + + host = HttpHost([serve, '--apps', manifest, '--workers', 2]) + try: + running = {app['name']: app for app in host.info['apps']} + assert [name for name, app in running.items() if app['running']] == ['a', 'b', 'busy', 'tight'], running + assert running['broken']['why'].startswith('Module:') and running['twin']['why'] == "The store is a's", running + port = host.port + assert post(port, '/a/inputs/add?step=2') == (200, 'none') + assert get(port, '/a/views/total')[::2] == (200, b'2') and get(port, '/b/views/total')[::2] == (200, b'0') + assert get(port, '/broken/views/total')[:2] == (503, 'stopped') and post(port, '/twin/inputs/add?step=1')[0] == 503 + assert get(port, '/nobody/views/total')[:2] == (404, 'no_app') and get(port, '/views/total')[:2] == (404, 'no_app') + # Work: ten items to walk; the tight app refuses the walk and changes nothing, a's runs it. + for app in ('a', 'tight'): + for k in range(10): + assert post(port, f'/{app}/inputs/fill?k={k}') == (200, 'none') + assert post(port, '/tight/inputs/walk?step=1') == (422, 'work') and get(port, '/tight/views/count')[::2] == (200, b'9') + assert post(port, '/a/inputs/walk?step=1') == (200, 'none') and get(port, '/a/views/count')[::2] == (200, b'10') + # Queue: requests at once, from as many connections; b answers meanwhile. + flood = [] + for _ in range(120): + connection = socket.create_connection(('127.0.0.1', port)) + flood.append(connection) + for connection in flood: + connection.sendall(b'POST /busy/inputs/add?step=1 HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\n\r\n') + assert post(port, '/b/inputs/add?step=5') == (200, 'none') + answers = [] + for connection in flood: + head = b'' + while b'\r\n\r\n' not in head: + head += connection.recv(4096) + answers.append(head.split(b' ', 2)[1]) + connection.close() + ok, busy = answers.count(b'200'), answers.count(b'503') + assert ok + busy == len(flood) and busy > 0 and ok > 0, (ok, busy) + assert get(port, '/busy/views/total')[::2] == (200, str(ok).encode()) # the refused changed nothing + # The store of b held by another process: b cannot save, a can. + with open(work / 'store-b' / '.writer.lock', 'r+b') as held: + fcntl.lockf(held, fcntl.LOCK_EX | fcntl.LOCK_NB) + assert post(port, '/b/inputs/add?step=1') == (503, 'store_busy') + assert post(port, '/a/inputs/add?step=1') == (200, 'none') + assert get(port, '/b/views/total')[::2] == (200, b'5') + fcntl.lockf(held, fcntl.LOCK_UN) + assert post(port, '/b/inputs/add?step=1') == (200, 'none') + finally: + stats = host.stop()[-1] + by_name = {app['name']: app for app in stats['apps']} + assert by_name['busy']['busy'] == busy and by_name['a']['events'] > 0 and stats['workers'] == 2, stats['apps'] + # Restarted: each app's states are back. + host = HttpHost([serve, '--apps', manifest]) + try: + assert get(host.port, '/a/views/total')[::2] == (200, b'3') and get(host.port, '/b/views/total')[::2] == (200, b'6') + assert get(host.port, '/a/views/count')[::2] == (200, b'10') and get(host.port, '/busy/views/total')[::2] == \ + (200, str(ok).encode()) + finally: + host.stop() + return {'apps': len(running), 'stopped': 2, 'flooded': len(flood), 'busy': busy} + + def main(): global EDITOR parser = argparse.ArgumentParser() @@ -3775,6 +3891,7 @@ def main(): report['server_answers_memory'] = server_answers_memory(aup, pure, work) report['server_meaning'] = server_meaning(aup, pure, work) report['server_work'] = server_work(aup, pure, work) + report['server_apps'] = server_apps(aup, pure, work) print(json.dumps(report)) |
