aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/connections/server.c
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-24 17:14:54 +0100
committereunhhu <[email protected]>2026-09-24 17:14:54 +0100
commitb1ce1927022d374198441afafeb27199fc8a052a (patch)
tree6021f175f4c67ed03b44240eaa2f90d846f12f6f /prototypes/connections/server.c
parentd34f9426d5317a1948f12f229439946728bc342a (diff)
downloadpure-b1ce1927022d374198441afafeb27199fc8a052a.tar.gz
pure-b1ce1927022d374198441afafeb27199fc8a052a.zip
Establish native micro runtime checkpoint
Diffstat (limited to 'prototypes/connections/server.c')
-rw-r--r--prototypes/connections/server.c645
1 files changed, 645 insertions, 0 deletions
diff --git a/prototypes/connections/server.c b/prototypes/connections/server.c
new file mode 100644
index 0000000..505bb71
--- /dev/null
+++ b/prototypes/connections/server.c
@@ -0,0 +1,645 @@
+#define _POSIX_C_SOURCE 200809L
+#include "store.h"
+#include "program.h"
+#include "store-port.h"
+#ifdef PURE_EMBEDDED_PROGRAM
+#include "embedded-plan.h"
+#endif
+#include <arpa/inet.h>
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <poll.h>
+#ifdef PURE_HTTP_ASYNC
+#include <pthread.h>
+#endif
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/socket.h>
+#include <time.h>
+#include <unistd.h>
+
+#if defined(PURE_HTTP_ASYNC) && !defined(PURE_ASYNC_EFFECTS)
+#error PURE_HTTP_ASYNC requires PURE_ASYNC_EFFECTS
+#endif
+
+enum { SLOTS = 8, HEADER_LIMIT = 4096, CHUNK = 65536 };
+typedef enum { READING_HEADER, READING_BODY, WRITING, WAITING_EFFECT } Phase;
+typedef struct Server Server;
+#ifdef PURE_HTTP_ASYNC
+typedef enum { JOB_FREE, JOB_QUEUED, JOB_RUNNING, JOB_DONE } JobState;
+typedef struct {
+ PureEffect effect;
+ PureResource resource;
+ PureSpan reply;
+ PureResult status;
+ int64_t revision;
+ JobState state;
+} EffectJob;
+typedef struct { Server *server; PureInput input; } EffectWorker;
+#endif
+typedef struct {
+ int fd;
+ Phase phase;
+ uint64_t deadline;
+ char request[HEADER_LIMIT + 1], response[512], key[65];
+ size_t header_size, body_size, received;
+ unsigned char *body;
+ bool put;
+ int64_t expected;
+ PureTicket ticket;
+ PureTransfer transfer;
+ Server *server;
+} Connection;
+struct Server {
+ const char *path;
+#ifndef PURE_EMBEDDED_PROGRAM
+ PureProgram program;
+#endif
+ PureRuntime runtime;
+ PureWork work[SLOTS];
+ size_t budget, active, peak_active;
+ uint64_t accepted, expired, prefix_copy_bytes;
+ uint64_t timeout_ms;
+ int send_buffer;
+ Connection connections[SLOTS];
+#ifdef PURE_HTTP_ASYNC
+ PureHost effect_host;
+ pthread_t worker[2];
+ EffectWorker worker_context[2];
+ pthread_mutex_t job_mutex;
+ pthread_cond_t job_ready[2];
+ EffectJob jobs[SLOTS];
+ size_t next_job[2];
+ int effect_wake;
+ bool worker_stop;
+#endif
+};
+static volatile sig_atomic_t stopping;
+static volatile sig_atomic_t wake_write = -1;
+static void stop_server(int sig) {
+ (void)sig;
+ int saved_errno = errno;
+ stopping = 1;
+ if (wake_write >= 0) {
+ const char byte = 1;
+ (void)write((int)wake_write, &byte, 1);
+ }
+ errno = saved_errno;
+}
+
+static uint64_t now_ms(void) {
+ struct timespec t;
+ if (clock_gettime(CLOCK_MONOTONIC, &t)) abort();
+ return (uint64_t)t.tv_sec * 1000 + (uint64_t)t.tv_nsec / 1000000;
+}
+
+static bool number(const char *text, uint64_t maximum, uint64_t *value) {
+ if (!*text) return false;
+ uint64_t n = 0;
+ for (; *text; ++text) {
+ if (*text < '0' || *text > '9') return false;
+ unsigned digit = (unsigned)(*text - '0');
+ if (digit > maximum || n > (maximum - digit) / 10) return false;
+ n = n * 10 + digit;
+ }
+ *value = n;
+ return true;
+}
+
+static void release_payload(void *owner) {
+ Connection *c = owner;
+ if (c->ticket.runtime) {
+ bool released = pure_runtime_release(c->ticket);
+ assert(released);
+ (void)released;
+ }
+ c->ticket = (PureTicket){0};
+ c->body = NULL;
+}
+
+static void close_connection(Connection *c) {
+ if (c->fd < 0) return;
+ if (c->transfer.active) pure_transfer_abort(&c->transfer);
+ else release_payload(c);
+ close(c->fd);
+ --c->server->active;
+ *c = (Connection){.fd = -1};
+}
+
+static void respond(Connection *c, int status, const char *reason, const char *error,
+ int64_t revision, PureSpan body) {
+ char tag[80] = "";
+ if (revision > 0) snprintf(tag, sizeof(tag), "ETag: \"%" PRId64 "\"\r\n", revision);
+ int length = snprintf(c->response, sizeof(c->response),
+ "HTTP/1.1 %d %s\r\nContent-Length: %zu\r\nConnection: close\r\n"
+ "Content-Type: application/octet-stream\r\nCache-Control: no-store\r\n"
+ "X-Pure-Error: %s\r\n%s\r\n", status, reason, body.size, error, tag);
+ assert(length > 0 && (size_t)length < sizeof(c->response));
+ c->phase = WRITING;
+ pure_transfer_init(&c->transfer,
+ (PureSpan){(const unsigned char *)c->response, (size_t)length}, body, c, release_payload);
+}
+
+static void fail(Connection *c, int status, const char *reason, const char *error) {
+ /* No outstanding backend access on the owner thread at this point. */
+ release_payload(c);
+ respond(c, status, reason, error, 0, (PureSpan){0});
+}
+
+static void answer(Connection *c, PureOutcome result) {
+ switch (result.status) {
+ case PURE_OK: respond(c, 200, "OK", "none", result.revision, result.body); break;
+ case PURE_NOT_FOUND: fail(c, 404, "Not Found", "not_found"); break;
+ case PURE_CONFLICT:
+ respond(c, 412, "Precondition Failed", "conflict", result.revision, (PureSpan){0}); break;
+ case PURE_BUSY: fail(c, 503, "Service Unavailable", "store_busy"); break;
+ case PURE_BUDGET: fail(c, 503, "Service Unavailable", "payload_budget"); break;
+ case PURE_ALLOCATION: fail(c, 503, "Service Unavailable", "allocation"); break;
+ case PURE_CLOSED: fail(c, 503, "Service Unavailable", "stopped"); break;
+ case PURE_DENIED: fail(c, 403, "Forbidden", "denied"); break;
+ case PURE_LIMIT: fail(c, 413, "Content Too Large", "limit"); break;
+ case PURE_UNCERTAIN: fail(c, 500, "Internal Server Error", "durability_unknown"); break;
+ default: fail(c, 500, "Internal Server Error", "storage"); break;
+ }
+}
+
+#ifdef PURE_HTTP_ASYNC
+/* Immutable reads may run beside one writer; only the write worker touches
+ * the store's temporary-name sequence and publication lock. Runtime and
+ * connections remain on poll's owner thread. Borrowed spans outlive work. */
+static void *effect_worker(void *context) {
+ EffectWorker *worker = context;
+ Server *server = worker->server;
+ PureInput input = worker->input;
+ for (;;) {
+ pthread_mutex_lock(&server->job_mutex);
+ size_t slot = SLOTS;
+ while (slot == SLOTS) {
+ for (size_t offset = 0; offset < SLOTS; ++offset) {
+ size_t candidate = (server->next_job[input] + offset) % SLOTS;
+ if (server->jobs[candidate].state == JOB_QUEUED &&
+ server->jobs[candidate].effect.input == input) {
+ slot = candidate;
+ break;
+ }
+ }
+ if (slot < SLOTS || server->worker_stop) break;
+ pthread_cond_wait(&server->job_ready[input], &server->job_mutex);
+ }
+ if (slot == SLOTS) { pthread_mutex_unlock(&server->job_mutex); return NULL; }
+ EffectJob *job = &server->jobs[slot];
+ server->next_job[input] = (slot + 1) % SLOTS;
+ job->state = JOB_RUNNING;
+ PureEffect effect = job->effect;
+ pthread_mutex_unlock(&server->job_mutex);
+
+ PureResource resource = {0};
+ PureSpan reply = {0};
+ int64_t revision = 0;
+ PureHost host = server->effect_host;
+ PureResult status = effect.input == PURE_INPUT_READ ?
+ host.read(host.context, server->runtime.definition, effect.key,
+ effect.available, &resource, &revision) :
+ host.write(host.context, server->runtime.definition, effect.key,
+ effect.expected, effect.value, &reply, &revision);
+
+ pthread_mutex_lock(&server->job_mutex);
+ job->resource = resource;
+ job->reply = reply;
+ job->revision = revision;
+ job->status = status;
+ job->state = JOB_DONE;
+ pthread_mutex_unlock(&server->job_mutex);
+ const char byte = 1;
+ (void)write(server->effect_wake, &byte, 1);
+ }
+}
+
+static void finish_effects(Server *server) {
+ for (size_t i = 0; i < SLOTS; ++i) {
+ pthread_mutex_lock(&server->job_mutex);
+ EffectJob *job = &server->jobs[i];
+ if (job->state != JOB_DONE) { pthread_mutex_unlock(&server->job_mutex); continue; }
+ PureTicket ticket = job->effect.ticket;
+ PureInput input = job->effect.input;
+ PureResource resource = job->resource;
+ PureSpan reply = job->reply;
+ PureResult status = job->status;
+ int64_t revision = job->revision;
+ *job = (EffectJob){0};
+ pthread_mutex_unlock(&server->job_mutex);
+ if (!pure_runtime_complete(ticket, status,
+ input == PURE_INPUT_READ ? &resource : NULL,
+ reply, revision)) abort();
+ for (size_t j = 0; j < SLOTS; ++j) {
+ Connection *c = &server->connections[j];
+ if (c->fd < 0 || c->phase != WAITING_EFFECT ||
+ c->ticket.slot != ticket.slot || c->ticket.generation != ticket.generation)
+ continue;
+ PureOutcome result;
+ if (!pure_runtime_result(ticket, &result)) abort();
+ c->body = NULL;
+ answer(c, result);
+ break;
+ }
+ }
+}
+#endif
+
+static void run_request(Connection *c) {
+ if (c->put && !pure_runtime_ready(c->ticket)) abort();
+#ifdef PURE_HTTP_ASYNC
+ Server *server = c->server;
+ PureEffect effect;
+ PureTake taken = pure_runtime_take(&server->runtime, &effect);
+ if (taken == PURE_TAKE_EFFECT) {
+ pthread_mutex_lock(&server->job_mutex);
+ EffectJob *job = &server->jobs[effect.ticket.slot];
+ if (job->state != JOB_FREE) abort();
+ *job = (EffectJob){.effect = effect, .state = JOB_QUEUED};
+ pthread_cond_signal(&server->job_ready[effect.input]);
+ pthread_mutex_unlock(&server->job_mutex);
+ c->phase = WAITING_EFFECT;
+ return;
+ }
+ abort();
+#else
+ PureOutcome result;
+ while (!pure_runtime_result(c->ticket, &result))
+ if (!pure_runtime_step(&c->server->runtime)) abort();
+#endif
+ c->body = NULL; /* Core now owns input until all consumers release it. */
+#ifdef PURE_HTTP_ASYNC
+ PureOutcome result;
+ if (!pure_runtime_result(c->ticket, &result)) abort();
+#endif
+ answer(c, result);
+}
+
+/* Deliberately bounded HTTP/1.1 subset: one GET/PUT, Content-Length, close.
+ * No transfer coding, pipelining, trailers, Expect, URL decoding, or proxying. */
+static bool parse_header(Connection *c, size_t end) {
+ for (size_t i = 0; i < end; ++i) {
+ unsigned char b = (unsigned char)c->request[i];
+ if ((b < 32 && b != '\r' && b != '\n' && b != '\t') || b == 127) return false;
+ }
+ char *line = c->request, *next = strstr(line, "\r\n");
+ if (!next) return false;
+ *next = 0;
+ char *path = strchr(line, ' ');
+ if (!path) return false;
+ *path++ = 0;
+ if (strcmp(line, "PUT") == 0) c->put = true;
+ else if (strcmp(line, "GET") != 0) return false;
+ char *version = strchr(path, ' ');
+ if (!version) return false;
+ *version++ = 0;
+ if (strcmp(version, "HTTP/1.1") || strncmp(path, "/objects/", 9)) return false;
+ const char *key = path + 9;
+ size_t key_size = strlen(key);
+ if (!key_size || key_size > 64) return false;
+ for (size_t i = 0; i < key_size; ++i) {
+ char b = key[i];
+ if (!((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') ||
+ (b >= '0' && b <= '9') || b == '-' || b == '_')) return false;
+ }
+ memcpy(c->key, key, key_size + 1);
+ bool length_seen = false, condition_seen = false, host_seen = false;
+ line = next + 2;
+ while (*line != '\r') {
+ next = strstr(line, "\r\n");
+ if (!next) return false;
+ *next = 0;
+ char *value = strchr(line, ':');
+ if (!value || value == line) return false;
+ *value++ = 0;
+ for (char *p = line; *p; ++p)
+ if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
+ (*p >= '0' && *p <= '9') || *p == '-')) return false;
+ while (*value == ' ' || *value == '\t') ++value;
+ char *tail = value + strlen(value);
+ while (tail > value && (tail[-1] == ' ' || tail[-1] == '\t')) *--tail = 0;
+ if (strchr(value, '\r') || strchr(value, '\n')) return false;
+ uint64_t n;
+ if (!strcasecmp(line, "Content-Length")) {
+ if (length_seen || !number(value, SIZE_MAX, &n)) return false;
+ length_seen = true;
+ c->body_size = (size_t)n;
+ } else if (!strcasecmp(line, "Host")) {
+ if (host_seen || !*value) return false;
+ host_seen = true;
+ } else if (!strcasecmp(line, "If-Match")) {
+ if (condition_seen || tail - value < 3 || *value != '"' || tail[-1] != '"') return false;
+ tail[-1] = 0;
+ if (!number(value + 1, INT64_MAX, &n) || !n) return false;
+ c->expected = (int64_t)n;
+ condition_seen = true;
+ } else if (!strcasecmp(line, "If-None-Match")) {
+ if (condition_seen || strcmp(value, "*")) return false;
+ c->expected = 0;
+ condition_seen = true;
+ } else if (!strcasecmp(line, "Transfer-Encoding") || !strcasecmp(line, "Expect")) return false;
+ line = next + 2;
+ }
+ return host_seen && (c->put ? length_seen && condition_seen : !condition_seen && c->body_size == 0);
+}
+
+static void read_connection(Connection *c) {
+ if (c->phase == READING_HEADER) {
+ ssize_t n = recv(c->fd, c->request + c->header_size, HEADER_LIMIT - c->header_size, 0);
+ if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) close_connection(c); return; }
+ if (!n) { close_connection(c); return; }
+ c->header_size += (size_t)n;
+ c->request[c->header_size] = 0;
+ /* Scan by byte count, so embedded NUL cannot hide a terminator. */
+ size_t end = 0;
+ for (size_t i = 3; i < c->header_size; ++i)
+ if (!memcmp(c->request + i - 3, "\r\n\r\n", 4)) { end = i + 1; break; }
+ if (!end) {
+ if (c->header_size == HEADER_LIMIT) fail(c, 431, "Request Header Fields Too Large", "headers");
+ return;
+ }
+ size_t prefix = c->header_size - end;
+ if (!parse_header(c, end)) { fail(c, 400, "Bad Request", "request"); return; }
+ if (c->body_size > PURE_OBJECT_LIMIT) { fail(c, 413, "Content Too Large", "limit"); return; }
+ if (prefix > c->body_size) { fail(c, 400, "Bad Request", "extra_body"); return; }
+ PureResult admitted = pure_runtime_accept(&c->server->runtime,
+ c->put ? PURE_INPUT_WRITE : PURE_INPUT_READ, c->key, c->expected, c->body_size, &c->ticket);
+ if (admitted != PURE_OK) { answer(c, (PureOutcome){.status = admitted}); return; }
+ if (!c->put) { run_request(c); return; }
+ c->body = pure_runtime_ingress(c->ticket, NULL);
+ if (prefix) memcpy(c->body, c->request + end, prefix);
+ c->server->prefix_copy_bytes += prefix;
+ c->received = prefix;
+ c->phase = READING_BODY;
+ if (c->received == c->body_size) run_request(c);
+ } else {
+ size_t remaining = c->body_size - c->received;
+ ssize_t n = recv(c->fd, c->body + c->received, remaining < CHUNK ? remaining : CHUNK, 0);
+ if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) close_connection(c); return; }
+ if (!n) { close_connection(c); return; }
+ c->received += (size_t)n;
+ if (c->received == c->body_size) run_request(c);
+ }
+}
+
+static void write_connection(Connection *c) {
+ PureSpan span = pure_transfer_next(&c->transfer, CHUNK);
+ ssize_t n = send(c->fd, span.data, span.size, 0);
+ if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) close_connection(c); return; }
+ if (!n) { close_connection(c); return; }
+ if (!pure_transfer_consume(&c->transfer, (size_t)n)) abort();
+ if (!c->transfer.active) close_connection(c);
+}
+
+static bool nonblocking(int fd) {
+ int flags = fcntl(fd, F_GETFL, 0);
+ return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
+}
+
+static bool close_on_exec(int fd) {
+ int flags = fcntl(fd, F_GETFD, 0);
+ return flags >= 0 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == 0;
+}
+
+int main(int argc, char **argv) {
+ Server server = {.budget = 4u * 1024u * 1024u, .timeout_ms = 10000};
+ unsigned port = 8767;
+#ifndef PURE_EMBEDDED_PROGRAM
+ const char *program_path = NULL;
+ bool explain = argc == 3 && !strcmp(argv[1], "--check-program");
+ char program_error[160];
+ if (explain) program_path = argv[2];
+#else
+ bool explain = false;
+#endif
+ for (int i = 1; !explain && i < argc; i += 2) {
+ if (i + 1 == argc) goto usage;
+ uint64_t value;
+ if (!strcmp(argv[i], "--store")) server.path = argv[i + 1];
+#ifndef PURE_EMBEDDED_PROGRAM
+ else if (!strcmp(argv[i], "--program")) program_path = argv[i + 1];
+#endif
+ else if (!strcmp(argv[i], "--port") && number(argv[i + 1], 65535, &value)) port = (unsigned)value;
+ else if (!strcmp(argv[i], "--budget") && number(argv[i + 1], SIZE_MAX, &value) && value) server.budget = (size_t)value;
+ else if (!strcmp(argv[i], "--timeout-ms") && number(argv[i + 1], 3600000, &value) && value) server.timeout_ms = value;
+ else if (!strcmp(argv[i], "--send-buffer") && number(argv[i + 1], INT32_MAX, &value)) server.send_buffer = (int)value;
+ else goto usage;
+ }
+ if (!server.path && !explain) goto usage;
+#ifndef PURE_EMBEDDED_PROGRAM
+ if (!program_path) goto usage;
+ if (!pure_program_load(program_path, &server.program, program_error, sizeof(program_error))) {
+ fprintf(stderr, "Program: %s\n", program_error); return 2;
+ }
+ if (explain) {
+ PureProgram *p = &server.program;
+ printf("{\"read\":{\"to_end\":%s,\"offset\":%zu,\"length\":%zu,\"minimum_source_bytes\":%zu},"
+ "\"write\":{\"to_end\":%s,\"offset\":%zu,\"length\":%zu,\"minimum_source_bytes\":%zu},"
+ "\"reply_input\":%s,\"view_definitions\":%zu,\"range_definitions\":%zu,"
+ "\"runtime_view_steps\":%zu,\"write_step\":%zu,\"reply_step\":%zu,\"prepared_program_bytes\":%zu,\"steps\":[",
+ p->read.to_end ? "true" : "false", p->read.offset, p->read.length, p->read_required,
+ p->write.to_end ? "true" : "false", p->write.offset, p->write.length, p->input_required,
+ p->reply_input ? "true" : "false", p->view_definitions, p->range_definitions,
+ p->input_steps, p->write_step, p->reply_step, sizeof(*p));
+ for (size_t i = 0; i < p->input_steps; ++i) {
+ const PureRangeStep *step = &p->steps[i];
+ printf("%s{\"source_step\":%zu,\"source\":{\"offset\":%zu,\"length\":%zu,\"to_end\":%s},"
+ "\"offset\":{\"value\":%zu,\"from_u32le\":%s},\"length\":{\"value\":%zu,\"from_u32le\":%s}}",
+ i ? "," : "", step->source_step, step->source.offset, step->source.length,
+ step->source.to_end ? "true" : "false", step->offset.value, step->offset.from_u32le ? "true" : "false",
+ step->length.value, step->length.from_u32le ? "true" : "false");
+ }
+ printf("],\"write_effect\":\"%s\",\"splice_offset\":{\"value\":%zu,\"from_u32le\":%s},"
+ "\"splice_remove\":{\"value\":%zu,\"from_u32le\":%s}}\n",
+ p->splice ? "splice" : "write", p->splice_offset.value,
+ p->splice_offset.from_u32le ? "true" : "false", p->splice_remove.value,
+ p->splice_remove.from_u32le ? "true" : "false");
+ return 0;
+ }
+#endif
+ const PureProgram *program =
+#ifdef PURE_EMBEDDED_PROGRAM
+ &pure_embedded_program;
+#else
+ &server.program;
+#endif
+ if (pure_store_init(server.path) != PURE_OK) { fputs("Cannot initialize byte store directory\n", stderr); return 1; }
+ PureHost host = pure_store_host(server.path);
+#ifdef PURE_HTTP_ASYNC
+ server.effect_host = host;
+ host.read = NULL;
+ host.write = NULL;
+#endif
+ if (!pure_runtime_init(&server.runtime, server.work, SLOTS, program,
+ host, server.budget)) return 1;
+#ifdef PURE_HTTP_ASYNC
+ if (!pure_runtime_enable_async(&server.runtime)) abort();
+#endif
+ signal(SIGPIPE, SIG_IGN);
+ int listener = socket(AF_INET, SOCK_STREAM, 0);
+ if (listener < 0) { perror("socket"); return 1; }
+ int one = 1;
+ setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+ struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port),
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
+ if (bind(listener, (struct sockaddr *)&address, sizeof(address)) || listen(listener, SLOTS) || !nonblocking(listener)) {
+ perror("listen"); close(listener); return 1;
+ }
+ socklen_t address_size = sizeof(address);
+ if (getsockname(listener, (struct sockaddr *)&address, &address_size)) { close(listener); return 1; }
+ int wake[2];
+ if (pipe(wake)) { perror("pipe"); close(listener); return 1; }
+ if (!nonblocking(wake[1]) || !close_on_exec(wake[0]) || !close_on_exec(wake[1])
+#ifdef PURE_HTTP_ASYNC
+ || !nonblocking(wake[0])
+#endif
+ ) {
+ perror("wake pipe"); close(wake[0]); close(wake[1]); close(listener); return 1;
+ }
+ wake_write = wake[1];
+ struct sigaction stop_action = {.sa_handler = stop_server};
+ sigemptyset(&stop_action.sa_mask);
+ if (sigaction(SIGINT, &stop_action, NULL) || sigaction(SIGTERM, &stop_action, NULL)) {
+ perror("sigaction"); wake_write = -1;
+ close(wake[0]); close(wake[1]); close(listener); return 1;
+ }
+#ifdef PURE_HTTP_ASYNC
+ server.effect_wake = wake[1];
+ pthread_attr_t worker_attr;
+ if (pthread_mutex_init(&server.job_mutex, NULL) ||
+ pthread_cond_init(&server.job_ready[PURE_INPUT_READ], NULL) ||
+ pthread_cond_init(&server.job_ready[PURE_INPUT_WRITE], NULL) ||
+ pthread_attr_init(&worker_attr) ||
+ pthread_attr_setstacksize(&worker_attr, 128u * 1024u)) {
+ fputs("Cannot initialize bounded storage workers\n", stderr);
+ wake_write = -1;
+ close(wake[0]); close(wake[1]); close(listener); return 1;
+ }
+ size_t workers_started = 0;
+ for (size_t i = 0; i < 2; ++i) {
+ server.worker_context[i] = (EffectWorker){.server = &server, .input = (PureInput)i};
+ if (pthread_create(&server.worker[i], &worker_attr,
+ effect_worker, &server.worker_context[i])) break;
+ ++workers_started;
+ }
+ pthread_attr_destroy(&worker_attr);
+ if (workers_started != 2) {
+ pthread_mutex_lock(&server.job_mutex);
+ server.worker_stop = true;
+ pthread_cond_signal(&server.job_ready[PURE_INPUT_READ]);
+ pthread_cond_signal(&server.job_ready[PURE_INPUT_WRITE]);
+ pthread_mutex_unlock(&server.job_mutex);
+ for (size_t i = 0; i < workers_started; ++i) pthread_join(server.worker[i], NULL);
+ pthread_cond_destroy(&server.job_ready[PURE_INPUT_READ]);
+ pthread_cond_destroy(&server.job_ready[PURE_INPUT_WRITE]);
+ pthread_mutex_destroy(&server.job_mutex);
+ fputs("Cannot start bounded storage workers\n", stderr);
+ wake_write = -1;
+ close(wake[0]); close(wake[1]); close(listener); return 1;
+ }
+#endif
+ for (size_t i = 0; i < SLOTS; ++i) server.connections[i].fd = -1;
+ printf("{\"port\":%u,\"storage\":\"immutable-byte-extents-v1\"}\n", ntohs(address.sin_port));
+ fflush(stdout);
+ while (!stopping) {
+ struct pollfd polls[SLOTS + 2] = {
+ {.fd = server.active < SLOTS ? listener : -1, .events = POLLIN},
+ {.fd = wake[0], .events = POLLIN}
+ };
+ uint64_t nearest_deadline = UINT64_MAX;
+ for (size_t i = 0; i < SLOTS; ++i) {
+ Connection *c = &server.connections[i];
+ polls[i + 2] = (struct pollfd){.fd = c->phase == WAITING_EFFECT ? -1 : c->fd,
+ .events = c->phase == WRITING ? POLLOUT : POLLIN};
+ if (c->fd >= 0 && c->deadline < nearest_deadline) nearest_deadline = c->deadline;
+ }
+ int wait_ms = -1;
+ if (nearest_deadline != UINT64_MAX) {
+ uint64_t now = now_ms();
+ uint64_t remaining = nearest_deadline > now ? nearest_deadline - now : 0;
+ wait_ms = remaining > INT_MAX ? INT_MAX : (int)remaining;
+ }
+ int ready = poll(polls, SLOTS + 2, wait_ms);
+ if (ready < 0 && errno != EINTR) { perror("poll"); stopping = 1; }
+#ifdef PURE_HTTP_ASYNC
+ if (polls[1].revents & POLLIN) {
+ char bytes[64];
+ while (read(wake[0], bytes, sizeof(bytes)) > 0) {}
+ }
+ finish_effects(&server);
+#endif
+ uint64_t now = now_ms();
+ for (size_t i = 0; i < SLOTS; ++i) {
+ Connection *c = &server.connections[i];
+ if (c->fd < 0) continue;
+ if (now >= c->deadline) { ++server.expired; close_connection(c); continue; }
+ short events = polls[i + 2].revents;
+ if (events & (POLLERR | POLLNVAL)) { close_connection(c); continue; }
+#ifdef PURE_HTTP_ASYNC
+ if (c->phase == WAITING_EFFECT) continue;
+#endif
+ if (events & (POLLIN | POLLOUT | POLLHUP)) {
+ if (c->phase == WRITING) write_connection(c); else read_connection(c);
+ }
+ }
+ if (!stopping && (polls[0].revents & POLLIN)) {
+ int fd = accept(listener, NULL, NULL);
+ if (fd < 0) continue;
+ if (!nonblocking(fd)) { close(fd); continue; }
+ if (server.send_buffer) setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &server.send_buffer, sizeof(server.send_buffer));
+ for (size_t i = 0; i < SLOTS; ++i) if (server.connections[i].fd < 0) {
+ server.connections[i] = (Connection){.fd = fd, .server = &server, .deadline = now_ms() + server.timeout_ms};
+ ++server.active;
+ ++server.accepted;
+ if (server.active > server.peak_active) server.peak_active = server.active;
+ break;
+ }
+ }
+ }
+ close(listener);
+ wake_write = -1;
+#ifndef PURE_HTTP_ASYNC
+ close(wake[0]);
+ close(wake[1]);
+#endif
+ pure_runtime_stop(&server.runtime);
+ for (size_t i = 0; i < SLOTS; ++i) close_connection(&server.connections[i]);
+#ifdef PURE_HTTP_ASYNC
+ pthread_mutex_lock(&server.job_mutex);
+ server.worker_stop = true;
+ pthread_cond_signal(&server.job_ready[PURE_INPUT_READ]);
+ pthread_cond_signal(&server.job_ready[PURE_INPUT_WRITE]);
+ pthread_mutex_unlock(&server.job_mutex);
+ for (size_t i = 0; i < 2; ++i) if (pthread_join(server.worker[i], NULL)) abort();
+ finish_effects(&server);
+ close(wake[0]);
+ close(wake[1]);
+ pthread_cond_destroy(&server.job_ready[PURE_INPUT_READ]);
+ pthread_cond_destroy(&server.job_ready[PURE_INPUT_WRITE]);
+ pthread_mutex_destroy(&server.job_mutex);
+#endif
+ fprintf(stderr, "{\"active\":%zu,\"retained_payload_bytes\":%zu,\"peak_retained_payload_bytes\":%zu,"
+ "\"payload_budget\":%zu,\"peak_connections\":%zu,\"accepted\":%" PRIu64 ",\"commits\":%" PRIu64 ","
+ "\"gets\":%" PRIu64 ",\"conflicts\":%" PRIu64 ",\"budget_rejections\":%" PRIu64 ","
+ "\"expired\":%" PRIu64 ",\"releases\":%" PRIu64 ",\"ingress_prefix_copy_bytes\":%" PRIu64 ","
+ "\"borrowed_output_bytes\":%" PRIu64 ",\"server_structure_bytes\":%zu}\n",
+ server.active, server.runtime.retained, server.runtime.peak_retained, server.budget, server.peak_active,
+ server.accepted, server.runtime.commits, server.runtime.gets, server.runtime.conflicts, server.runtime.budget_rejections,
+ server.expired, server.runtime.releases, server.prefix_copy_bytes, server.runtime.borrowed_output_bytes, sizeof(server));
+ return 0;
+usage:
+#ifdef PURE_EMBEDDED_PROGRAM
+ fputs("pure-connections-fixed --store DIRECTORY [--port 8767] [--budget BYTES] [--timeout-ms 10000] [--send-buffer BYTES]\n", stderr);
+#else
+ fputs("pure-connections --program FILE --store DIRECTORY [--port 8767] [--budget BYTES] [--timeout-ms 10000] [--send-buffer BYTES]\n"
+ "pure-connections --check-program FILE\n", stderr);
+#endif
+ return 2;
+}