@sproutboat/runtime 0.19.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +273 -69
- package/src/transport-broker.js +566 -56
- package/src/transport-embedded.js +4 -0
- package/test/native-service-bindings.ts +346 -0
package/src/transport-broker.js
CHANGED
|
@@ -11,14 +11,20 @@
|
|
|
11
11
|
// If SB_BROKER_PORT is unset the shims below are never installed (compile.ts
|
|
12
12
|
// only emits the __sbInstallBindings call when the project declares bindings),
|
|
13
13
|
// so a plain sprout is byte-for-byte unchanged.
|
|
14
|
-
// ponytail:
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// the
|
|
18
|
-
//
|
|
14
|
+
// ponytail: still AF_INET loopback, not AF_UNIX. A failed v0 exchange reconnects
|
|
15
|
+
// and resends; the broker deduplicates its writes (queue.send, INSERT) by request
|
|
16
|
+
// id. v1 exchanges (fetch, service.fetch, R2) resend only when the broker cannot
|
|
17
|
+
// have acted: the request never fully left, or a reused connection had gone
|
|
18
|
+
// stale. On a host with an event loop, fetch and service.fetch run off it, on
|
|
19
|
+
// one I/O thread (sb_async_submit_v1).
|
|
19
20
|
|
|
20
21
|
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
21
22
|
Porffor.c`
|
|
23
|
+
#include <fcntl.h>
|
|
24
|
+
#include <poll.h>
|
|
25
|
+
#include <pthread.h>
|
|
26
|
+
#include <time.h>
|
|
27
|
+
|
|
22
28
|
// baronunread/sproutboat#176 — defined in render.js, alongside
|
|
23
29
|
// porf_native_fetch_read_value (which every inline-C block below already
|
|
24
30
|
// calls with no forward declaration of its own — that one gets one from
|
|
@@ -45,7 +51,8 @@ static int sb_io_all(int fd, unsigned char* buf, size_t len, int writing) {
|
|
|
45
51
|
// connect() handshake or close() each time. -1 = not connected.
|
|
46
52
|
static int sb_broker_fd = -1;
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
// A new connection to the broker: its fd, or a negative code.
|
|
55
|
+
static int sb_broker_open(void) {
|
|
49
56
|
const char* port_s = getenv("SB_BROKER_PORT");
|
|
50
57
|
if (!port_s) return -10;
|
|
51
58
|
signal(SIGPIPE, SIG_IGN); // a dead broker must yield EPIPE, not kill the sprout
|
|
@@ -59,16 +66,23 @@ static int sb_broker_connect(void) {
|
|
|
59
66
|
addr.sin_port = htons((unsigned short)atoi(port_s));
|
|
60
67
|
addr.sin_addr.s_addr = htonl(0x7f000001u); // 127.0.0.1
|
|
61
68
|
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { close(fd); return -2; }
|
|
69
|
+
return fd;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static int sb_broker_connect(void) {
|
|
73
|
+
int fd = sb_broker_open();
|
|
74
|
+
if (fd < 0) return fd;
|
|
62
75
|
sb_broker_fd = fd;
|
|
63
76
|
return 0;
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
// Send one
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
79
|
+
// Send one payload verbatim on fd and read one reply. The caller owns the
|
|
80
|
+
// payload's shape: a v0 exchange prepends the token line, a v1 one carries the
|
|
81
|
+
// token in its JSON and must reach the broker byte for byte — prefixing it would
|
|
82
|
+
// leave the marker in the wrong place and the frame would read as v0.
|
|
83
|
+
// -3 and -5 mean the broker never had the whole request; -4 and -6 mean it did,
|
|
84
|
+
// and may have acted on it.
|
|
85
|
+
static int sb_broker_exchange_fd(int fd, const char* body, size_t body_len, char** resp_out, size_t* resp_len_out) {
|
|
72
86
|
unsigned char* frame = (unsigned char*)malloc(4 + body_len);
|
|
73
87
|
if (!frame) return -5;
|
|
74
88
|
frame[0] = (unsigned char)(body_len & 0xff);
|
|
@@ -76,23 +90,28 @@ static int sb_broker_exchange_raw(const char* body, size_t body_len, char** resp
|
|
|
76
90
|
frame[2] = (unsigned char)((body_len >> 16) & 0xff);
|
|
77
91
|
frame[3] = (unsigned char)((body_len >> 24) & 0xff);
|
|
78
92
|
if (body_len) memcpy(frame + 4, body, body_len);
|
|
79
|
-
int wr = sb_io_all(
|
|
93
|
+
int wr = sb_io_all(fd, frame, 4 + body_len, 1);
|
|
80
94
|
free(frame);
|
|
81
95
|
if (wr != 0) return -3;
|
|
82
96
|
|
|
83
97
|
unsigned char rhdr[4];
|
|
84
|
-
if (sb_io_all(
|
|
98
|
+
if (sb_io_all(fd, rhdr, 4, 0) != 0) return -4;
|
|
85
99
|
size_t rlen = (size_t)rhdr[0] | ((size_t)rhdr[1] << 8) | ((size_t)rhdr[2] << 16) | ((size_t)rhdr[3] << 24);
|
|
86
100
|
|
|
87
101
|
char* buf = (char*)malloc(rlen ? rlen : 1);
|
|
88
102
|
if (!buf) return -5;
|
|
89
|
-
if (rlen && sb_io_all(
|
|
103
|
+
if (rlen && sb_io_all(fd, (unsigned char*)buf, rlen, 0) != 0) { free(buf); return -6; }
|
|
90
104
|
|
|
91
105
|
*resp_out = buf;
|
|
92
106
|
*resp_len_out = rlen;
|
|
93
107
|
return 0;
|
|
94
108
|
}
|
|
95
109
|
|
|
110
|
+
// The same exchange on the persistent connection.
|
|
111
|
+
static int sb_broker_exchange_raw(const char* body, size_t body_len, char** resp_out, size_t* resp_len_out) {
|
|
112
|
+
return sb_broker_exchange_fd(sb_broker_fd, body, body_len, resp_out, resp_len_out);
|
|
113
|
+
}
|
|
114
|
+
|
|
96
115
|
// #63 — the binary reply from the last v1 exchange, handed to JS on request.
|
|
97
116
|
// Stashed rather than returned inline so an 8 MB object body is one allocation
|
|
98
117
|
// in the Porffor heap, not a substring of a bigger one.
|
|
@@ -207,61 +226,488 @@ static int sb_req_is_fetch(const char* req, size_t n) {
|
|
|
207
226
|
return 0;
|
|
208
227
|
}
|
|
209
228
|
|
|
210
|
-
// A v1
|
|
211
|
-
|
|
212
|
-
static int sb_broker_roundtrip_v1(const char* json, size_t json_len, const char* body, size_t body_len,
|
|
213
|
-
char** resp_out, size_t* resp_len_out) {
|
|
229
|
+
// A v1 request: marker, json length, json, then the body bytes. malloc'd.
|
|
230
|
+
static char* sb_v1_request(const char* json, size_t json_len, const char* body, size_t body_len, size_t* out_len) {
|
|
214
231
|
size_t req_len = 1 + 4 + json_len + body_len;
|
|
215
232
|
char* req = (char*)malloc(req_len);
|
|
216
|
-
if (!req) return
|
|
233
|
+
if (!req) return 0;
|
|
217
234
|
req[0] = 1;
|
|
218
235
|
unsigned int jl = (unsigned int)json_len;
|
|
219
236
|
memcpy(req + 1, &jl, 4);
|
|
220
237
|
memcpy(req + 5, json, json_len);
|
|
221
238
|
if (body_len) memcpy(req + 5 + json_len, body, body_len);
|
|
239
|
+
*out_len = req_len;
|
|
240
|
+
return req;
|
|
241
|
+
}
|
|
222
242
|
|
|
223
|
-
|
|
243
|
+
// Splits a v1 reply, which it takes ownership of, into its JSON (returned,
|
|
244
|
+
// NUL-terminated, malloc'd) and its bytes (*bin, malloc'd, or 0 when there are
|
|
245
|
+
// none). Marker 1 is a final frame, 2 one of a streamed response's earlier ones. A
|
|
246
|
+
// broker that answered v0 to a v1 request predates this: its reply is returned
|
|
247
|
+
// whole, so the error it wrote is what the handler sees. 0 on OOM.
|
|
248
|
+
static char* sb_v1_split(char* resp, size_t resp_len, size_t* json_len, char** bin, size_t* bin_len) {
|
|
249
|
+
*bin = 0;
|
|
250
|
+
*bin_len = 0;
|
|
251
|
+
if (resp_len >= 5 && ((unsigned char)resp[0] == 1 || (unsigned char)resp[0] == 2)) {
|
|
252
|
+
unsigned int rjl = 0;
|
|
253
|
+
memcpy(&rjl, resp + 1, 4);
|
|
254
|
+
if (5 + (size_t)rjl <= resp_len) {
|
|
255
|
+
size_t n = resp_len - 5 - rjl;
|
|
256
|
+
if (n) {
|
|
257
|
+
*bin = (char*)malloc(n);
|
|
258
|
+
if (*bin) { memcpy(*bin, resp + 5 + rjl, n); *bin_len = n; }
|
|
259
|
+
}
|
|
260
|
+
char* json = (char*)malloc(rjl + 1);
|
|
261
|
+
if (json) { memcpy(json, resp + 5, rjl); json[rjl] = 0; *json_len = rjl; }
|
|
262
|
+
free(resp);
|
|
263
|
+
return json;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
*json_len = resp_len;
|
|
267
|
+
return resp;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// One v1 exchange on *fd (-1 = connect first), with up to four tries. A failure
|
|
271
|
+
// before the broker had the whole request is always retried. One after it is
|
|
272
|
+
// retried only on a connection that had served an earlier exchange, which may
|
|
273
|
+
// simply have gone stale (a broker restart); on a fresh one the broker may have
|
|
274
|
+
// acted on it, and fetch and service.fetch are not deduplicated by id.
|
|
275
|
+
static int sb_v1_send(int* fd, const char* req, size_t req_len, char** resp, size_t* resp_len) {
|
|
224
276
|
int rc = -3;
|
|
225
277
|
for (int attempt = 0; attempt < 4; attempt++) {
|
|
226
278
|
sb_backoff(attempt);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
279
|
+
int reused = *fd >= 0;
|
|
280
|
+
if (!reused) {
|
|
281
|
+
int c = sb_broker_open();
|
|
282
|
+
if (c < 0) { rc = c; continue; }
|
|
283
|
+
*fd = c;
|
|
230
284
|
}
|
|
231
|
-
rc =
|
|
232
|
-
if (rc == 0)
|
|
233
|
-
close(
|
|
234
|
-
|
|
285
|
+
rc = sb_broker_exchange_fd(*fd, req, req_len, resp, resp_len);
|
|
286
|
+
if (rc == 0) return 0;
|
|
287
|
+
close(*fd);
|
|
288
|
+
*fd = -1;
|
|
289
|
+
if (!reused && rc != -3 && rc != -5) return rc;
|
|
235
290
|
}
|
|
291
|
+
return rc;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// A v1 exchange on the persistent connection. The reply's JSON is returned and
|
|
295
|
+
// its bytes stashed for __sbTakeBin.
|
|
296
|
+
static int sb_broker_roundtrip_v1(const char* json, size_t json_len, const char* body, size_t body_len,
|
|
297
|
+
char** resp_out, size_t* resp_len_out) {
|
|
298
|
+
size_t req_len = 0;
|
|
299
|
+
char* req = sb_v1_request(json, json_len, body, body_len, &req_len);
|
|
300
|
+
if (!req) return -4;
|
|
301
|
+
char* resp = 0; size_t resp_len = 0;
|
|
302
|
+
int rc = sb_v1_send(&sb_broker_fd, req, req_len, &resp, &resp_len);
|
|
236
303
|
free(req);
|
|
237
304
|
if (rc != 0) return rc;
|
|
238
|
-
|
|
239
305
|
if (sb_bin_reply) { free(sb_bin_reply); sb_bin_reply = 0; sb_bin_reply_len = 0; }
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
306
|
+
char* json_only = sb_v1_split(resp, resp_len, resp_len_out, &sb_bin_reply, &sb_bin_reply_len);
|
|
307
|
+
if (!json_only) return -4;
|
|
308
|
+
*resp_out = json_only;
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Async exchanges, for a host with an event loop (the standalone server). The
|
|
313
|
+
// host installs a waker with sb_async_set_waker(); fetch() and service bindings
|
|
314
|
+
// then hand each exchange to one I/O thread and return at once, so the sprout
|
|
315
|
+
// keeps serving while calls are in flight — including the calls back into itself
|
|
316
|
+
// that recursion makes, however deep. The I/O thread multiplexes non-blocking
|
|
317
|
+
// broker connections with poll(), one per exchange in flight, and keeps a few
|
|
318
|
+
// idle ones for reuse. A finished exchange is queued and the waker called; the
|
|
319
|
+
// host then runs sb_async_drain() on its loop, which fires the JS callback
|
|
320
|
+
// registered under the request's timer id. A streamed response is delivered frame
|
|
321
|
+
// by frame (its head, then each chunk), each firing the callback, which the JS side
|
|
322
|
+
// registers as an interval for that. The thread touches only malloc'd jobs, never
|
|
323
|
+
// the JS heap. The same contract as the embedded transport's B18, which signals a
|
|
324
|
+
// pipe.
|
|
325
|
+
void porf_native_fetch_fire_timer(u32 id);
|
|
326
|
+
void porf_native_fetch_timer_clear(u32 id);
|
|
327
|
+
typedef struct sb_async_job {
|
|
328
|
+
struct sb_async_job* next;
|
|
329
|
+
u32 id;
|
|
330
|
+
// The framed request ([u32 LE length][v1 payload]) and how much of it is sent.
|
|
331
|
+
unsigned char* out; size_t out_len, out_off;
|
|
332
|
+
// The reply: its length prefix, then its payload.
|
|
333
|
+
unsigned char hdr[4]; size_t hdr_got;
|
|
334
|
+
char* in; size_t in_len, in_got;
|
|
335
|
+
int fd, reused, attempt, connecting;
|
|
336
|
+
// Asked for a streamed response; and has delivered part of one, so never retried.
|
|
337
|
+
int stream, delivered;
|
|
338
|
+
long long retry_at; // ms; 0 = not waiting to retry
|
|
339
|
+
char* json; size_t json_len;
|
|
340
|
+
char* bin; size_t bin_len;
|
|
341
|
+
} sb_async_job;
|
|
342
|
+
// One frame's worth of reply for the loop: a whole response, or part of a streamed one.
|
|
343
|
+
typedef struct sb_async_delivery {
|
|
344
|
+
struct sb_async_delivery* next;
|
|
345
|
+
u32 id;
|
|
346
|
+
int final;
|
|
347
|
+
char* json; size_t json_len;
|
|
348
|
+
char* bin; size_t bin_len;
|
|
349
|
+
} sb_async_delivery;
|
|
350
|
+
// Idle connections kept for the next exchanges.
|
|
351
|
+
#define SB_IO_IDLE 8
|
|
352
|
+
static pthread_mutex_t sb_async_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
353
|
+
static sb_async_job* sb_async_queue = 0;
|
|
354
|
+
static sb_async_delivery *sb_async_ready = 0, *sb_async_current = 0;
|
|
355
|
+
static int sb_io_started = 0, sb_io_wake[2] = { -1, -1 };
|
|
356
|
+
// Ids of exchanges whose caller gave up (an aborted signal), for the I/O thread.
|
|
357
|
+
static u32* sb_async_cancels = 0;
|
|
358
|
+
static size_t sb_async_cancels_n = 0, sb_async_cancels_cap = 0;
|
|
359
|
+
static void (*sb_async_waker)(void) = 0;
|
|
360
|
+
|
|
361
|
+
void sb_async_set_waker(void (*wake)(void)) { sb_async_waker = wake; }
|
|
362
|
+
int sb_async_enabled(void) { return sb_async_waker != 0; }
|
|
363
|
+
|
|
364
|
+
static long long sb_now_ms(void) {
|
|
365
|
+
struct timespec ts;
|
|
366
|
+
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
367
|
+
return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
static void sb_async_error(sb_async_job* j, int rc) {
|
|
371
|
+
char e[48];
|
|
372
|
+
int n = snprintf(e, sizeof(e), "{\"ok\":false,\"error\":\"broker rc %d\"}", rc);
|
|
373
|
+
j->json = (char*)malloc((size_t)n + 1);
|
|
374
|
+
if (!j->json) abort();
|
|
375
|
+
memcpy(j->json, e, (size_t)n + 1);
|
|
376
|
+
j->json_len = (size_t)n;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Queues a reply frame for the loop, which takes ownership of json and bin.
|
|
380
|
+
static void sb_async_deliver(u32 id, int final, char* json, size_t json_len, char* bin, size_t bin_len) {
|
|
381
|
+
sb_async_delivery* d = (sb_async_delivery*)calloc(1, sizeof(sb_async_delivery));
|
|
382
|
+
if (!d) abort();
|
|
383
|
+
d->id = id; d->final = final;
|
|
384
|
+
d->json = json; d->json_len = json_len;
|
|
385
|
+
d->bin = bin; d->bin_len = bin_len;
|
|
386
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
387
|
+
d->next = sb_async_ready;
|
|
388
|
+
sb_async_ready = d;
|
|
389
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
390
|
+
sb_async_waker();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Hands a finished job (json set) back to the loop, and frees it.
|
|
394
|
+
static void sb_async_finish(sb_async_job* j) {
|
|
395
|
+
sb_async_deliver(j->id, 1, j->json, j->json_len, j->bin, j->bin_len);
|
|
396
|
+
free(j->out);
|
|
397
|
+
free(j->in);
|
|
398
|
+
free(j);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// A reply frame's payload -> its ASCII JSON and bytes (see sb_json_ascii). 0 on OOM.
|
|
402
|
+
static char* sb_io_reply_json(char* payload, size_t len, size_t* json_len, char** bin, size_t* bin_len) {
|
|
403
|
+
char* json = sb_v1_split(payload, len, json_len, bin, bin_len);
|
|
404
|
+
size_t al = 0;
|
|
405
|
+
char* ascii = json ? sb_json_ascii(json, *json_len, &al) : 0;
|
|
406
|
+
if (ascii) { free(json); json = ascii; *json_len = al; }
|
|
407
|
+
return json;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
static void sb_io_nonblock(int fd) { fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); fcntl(fd, F_SETFD, FD_CLOEXEC); }
|
|
411
|
+
|
|
412
|
+
// Starts (or restarts) a job: an idle connection if there is one, else a new one.
|
|
413
|
+
static void sb_io_begin(sb_async_job* j, int* idle, int* idle_n) {
|
|
414
|
+
j->out_off = 0; j->hdr_got = 0; j->in_got = 0; j->in_len = 0;
|
|
415
|
+
free(j->in); j->in = 0;
|
|
416
|
+
j->retry_at = 0; j->connecting = 0;
|
|
417
|
+
if (*idle_n > 0) { j->fd = idle[--*idle_n]; j->reused = 1; return; }
|
|
418
|
+
j->reused = 0;
|
|
419
|
+
const char* port_s = getenv("SB_BROKER_PORT");
|
|
420
|
+
int fd = port_s ? socket(AF_INET, SOCK_STREAM, 0) : -1;
|
|
421
|
+
if (fd < 0) { j->fd = -1; return; }
|
|
422
|
+
int one = 1;
|
|
423
|
+
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
|
|
424
|
+
sb_io_nonblock(fd);
|
|
425
|
+
struct sockaddr_in addr;
|
|
426
|
+
memset(&addr, 0, sizeof(addr));
|
|
427
|
+
addr.sin_family = AF_INET;
|
|
428
|
+
addr.sin_port = htons((unsigned short)atoi(port_s));
|
|
429
|
+
addr.sin_addr.s_addr = htonl(0x7f000001u);
|
|
430
|
+
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
|
|
431
|
+
if (errno != EINPROGRESS) { close(fd); j->fd = -1; return; }
|
|
432
|
+
j->connecting = 1;
|
|
433
|
+
}
|
|
434
|
+
j->fd = fd;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// A failed attempt. Retried (after a backoff) when the broker never had the whole
|
|
438
|
+
// request, or the connection was a reused one that may have gone stale; failed
|
|
439
|
+
// otherwise, since fetch and service.fetch are not deduplicated by id.
|
|
440
|
+
static int sb_io_failed(sb_async_job* j, int rc) {
|
|
441
|
+
if (j->fd >= 0) { close(j->fd); j->fd = -1; }
|
|
442
|
+
int unsent = j->out_off < j->out_len;
|
|
443
|
+
if (!j->delivered && (unsent || j->reused) && ++j->attempt < 4) {
|
|
444
|
+
static const long ms[4] = { 0, 5, 25, 100 };
|
|
445
|
+
j->retry_at = sb_now_ms() + ms[j->attempt];
|
|
446
|
+
return 0;
|
|
447
|
+
}
|
|
448
|
+
sb_async_error(j, rc);
|
|
449
|
+
return 1;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Moves a job along as far as its socket allows. 1 = done (json set), 0 = not yet.
|
|
453
|
+
static int sb_io_step(sb_async_job* j, int* idle, int* idle_n) {
|
|
454
|
+
if (j->connecting) {
|
|
455
|
+
int err = 0; socklen_t len = sizeof(err);
|
|
456
|
+
if (getsockopt(j->fd, SOL_SOCKET, SO_ERROR, &err, &len) != 0 || err != 0) return sb_io_failed(j, -2);
|
|
457
|
+
j->connecting = 0;
|
|
458
|
+
}
|
|
459
|
+
while (j->out_off < j->out_len) {
|
|
460
|
+
long n = write(j->fd, j->out + j->out_off, j->out_len - j->out_off);
|
|
461
|
+
if (n > 0) { j->out_off += (size_t)n; continue; }
|
|
462
|
+
if (n < 0 && errno == EINTR) continue;
|
|
463
|
+
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0;
|
|
464
|
+
return sb_io_failed(j, -3);
|
|
465
|
+
}
|
|
466
|
+
for (;;) {
|
|
467
|
+
while (j->hdr_got < 4) {
|
|
468
|
+
long n = read(j->fd, j->hdr + j->hdr_got, 4 - j->hdr_got);
|
|
469
|
+
if (n > 0) { j->hdr_got += (size_t)n; continue; }
|
|
470
|
+
if (n < 0 && errno == EINTR) continue;
|
|
471
|
+
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0;
|
|
472
|
+
return sb_io_failed(j, -4);
|
|
473
|
+
}
|
|
474
|
+
if (!j->in) {
|
|
475
|
+
j->in_len = (size_t)j->hdr[0] | ((size_t)j->hdr[1] << 8) | ((size_t)j->hdr[2] << 16) | ((size_t)j->hdr[3] << 24);
|
|
476
|
+
j->in = (char*)malloc(j->in_len ? j->in_len : 1);
|
|
477
|
+
if (!j->in) abort();
|
|
478
|
+
}
|
|
479
|
+
while (j->in_got < j->in_len) {
|
|
480
|
+
long n = read(j->fd, j->in + j->in_got, j->in_len - j->in_got);
|
|
481
|
+
if (n > 0) { j->in_got += (size_t)n; continue; }
|
|
482
|
+
if (n < 0 && errno == EINTR) continue;
|
|
483
|
+
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0;
|
|
484
|
+
return sb_io_failed(j, -6);
|
|
485
|
+
}
|
|
486
|
+
char* payload = j->in;
|
|
487
|
+
size_t payload_len = j->in_len;
|
|
488
|
+
j->in = 0; j->in_len = 0; j->in_got = 0; j->hdr_got = 0;
|
|
489
|
+
int more = j->stream && payload_len >= 1 && (unsigned char)payload[0] == 2;
|
|
490
|
+
if (more) {
|
|
491
|
+
// Part of a streamed response: to the loop now, then on to the next frame.
|
|
492
|
+
size_t json_len = 0, bin_len = 0;
|
|
493
|
+
char* bin = 0;
|
|
494
|
+
char* json = sb_io_reply_json(payload, payload_len, &json_len, &bin, &bin_len);
|
|
495
|
+
if (!json) abort();
|
|
496
|
+
j->delivered = 1;
|
|
497
|
+
sb_async_deliver(j->id, 0, json, json_len, bin, bin_len);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (*idle_n < SB_IO_IDLE) idle[(*idle_n)++] = j->fd; else close(j->fd);
|
|
501
|
+
j->fd = -1;
|
|
502
|
+
j->json = sb_io_reply_json(payload, payload_len, &j->json_len, &j->bin, &j->bin_len);
|
|
503
|
+
if (!j->json) sb_async_error(j, -4);
|
|
504
|
+
return 1;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
static void* sb_io_thread(void* unused) {
|
|
509
|
+
(void)unused;
|
|
510
|
+
sb_async_job* active = 0;
|
|
511
|
+
int idle[SB_IO_IDLE], idle_n = 0;
|
|
512
|
+
struct pollfd* fds = 0;
|
|
513
|
+
sb_async_job** owners = 0;
|
|
514
|
+
size_t cap = 0;
|
|
515
|
+
for (;;) {
|
|
516
|
+
// New jobs.
|
|
517
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
518
|
+
sb_async_job* fresh = sb_async_queue;
|
|
519
|
+
sb_async_queue = 0;
|
|
520
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
521
|
+
while (fresh) {
|
|
522
|
+
sb_async_job* j = fresh;
|
|
523
|
+
fresh = j->next;
|
|
524
|
+
sb_io_begin(j, idle, &idle_n);
|
|
525
|
+
j->next = active;
|
|
526
|
+
active = j;
|
|
527
|
+
}
|
|
528
|
+
// Exchanges given up on: their connections close, which tells the broker to stop.
|
|
529
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
530
|
+
u32* cancels = sb_async_cancels;
|
|
531
|
+
size_t cancels_n = sb_async_cancels_n;
|
|
532
|
+
sb_async_cancels = 0;
|
|
533
|
+
sb_async_cancels_n = sb_async_cancels_cap = 0;
|
|
534
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
535
|
+
for (size_t c = 0; c < cancels_n; c++) {
|
|
536
|
+
for (sb_async_job** link = &active; *link; link = &(*link)->next) {
|
|
537
|
+
sb_async_job* j = *link;
|
|
538
|
+
if (j->id != cancels[c]) continue;
|
|
539
|
+
*link = j->next;
|
|
540
|
+
if (j->fd >= 0) close(j->fd);
|
|
541
|
+
free(j->out); free(j->in); free(j->json); free(j->bin); free(j);
|
|
542
|
+
break;
|
|
248
543
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
*
|
|
255
|
-
|
|
256
|
-
|
|
544
|
+
}
|
|
545
|
+
free(cancels);
|
|
546
|
+
// Retries that are due restart; a job with no socket fails, or waits to retry.
|
|
547
|
+
long long now = sb_now_ms();
|
|
548
|
+
for (sb_async_job** link = &active; *link;) {
|
|
549
|
+
sb_async_job* j = *link;
|
|
550
|
+
if (j->retry_at && j->retry_at <= now) sb_io_begin(j, idle, &idle_n);
|
|
551
|
+
if (!j->retry_at && j->fd < 0 && sb_io_failed(j, -2)) {
|
|
552
|
+
*link = j->next;
|
|
553
|
+
sb_async_finish(j);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
link = &j->next;
|
|
557
|
+
}
|
|
558
|
+
long long soonest = 0;
|
|
559
|
+
size_t count = 0;
|
|
560
|
+
for (sb_async_job* j = active; j; j = j->next) {
|
|
561
|
+
if (!j->retry_at) count++;
|
|
562
|
+
else if (!soonest || j->retry_at < soonest) soonest = j->retry_at;
|
|
563
|
+
}
|
|
564
|
+
if (count + 1 > cap) {
|
|
565
|
+
cap = (count + 1) * 2;
|
|
566
|
+
fds = (struct pollfd*)realloc(fds, cap * sizeof(struct pollfd));
|
|
567
|
+
owners = (sb_async_job**)realloc(owners, cap * sizeof(sb_async_job*));
|
|
568
|
+
if (!fds || !owners) abort();
|
|
569
|
+
}
|
|
570
|
+
fds[0].fd = sb_io_wake[0]; fds[0].events = POLLIN; fds[0].revents = 0;
|
|
571
|
+
size_t n = 1;
|
|
572
|
+
for (sb_async_job* j = active; j; j = j->next) {
|
|
573
|
+
if (j->retry_at) continue;
|
|
574
|
+
fds[n].fd = j->fd;
|
|
575
|
+
fds[n].events = j->connecting || j->out_off < j->out_len ? POLLOUT : POLLIN;
|
|
576
|
+
fds[n].revents = 0;
|
|
577
|
+
owners[n++] = j;
|
|
578
|
+
}
|
|
579
|
+
int timeout = soonest ? (int)(soonest > now ? soonest - now : 0) : -1;
|
|
580
|
+
if (poll(fds, (nfds_t)n, timeout) < 0 && errno != EINTR) continue;
|
|
581
|
+
if (fds[0].revents) { char buf[64]; while (read(sb_io_wake[0], buf, sizeof(buf)) > 0) {} }
|
|
582
|
+
for (size_t i = 1; i < n; i++) {
|
|
583
|
+
if (!fds[i].revents) continue;
|
|
584
|
+
sb_async_job* j = owners[i];
|
|
585
|
+
if (!sb_io_step(j, idle, &idle_n)) continue;
|
|
586
|
+
// Done: unlink and hand back.
|
|
587
|
+
sb_async_job** link = &active;
|
|
588
|
+
while (*link != j) link = &(*link)->next;
|
|
589
|
+
*link = j->next;
|
|
590
|
+
sb_async_finish(j);
|
|
257
591
|
}
|
|
258
592
|
}
|
|
259
|
-
// A broker that answered v0 to a v1 request predates this: pass its reply
|
|
260
|
-
// through so the error it wrote is what the handler sees.
|
|
261
|
-
*resp_out = resp;
|
|
262
|
-
*resp_len_out = resp_len;
|
|
263
593
|
return 0;
|
|
264
594
|
}
|
|
595
|
+
|
|
596
|
+
// The inline C of __sbCallAsync calls this by prototype: Porffor may emit a
|
|
597
|
+
// function's body before this block.
|
|
598
|
+
void sb_async_submit_v1(u32 id, int stream, const char* json, size_t json_len, const char* body, size_t body_len) {
|
|
599
|
+
sb_async_job* j = (sb_async_job*)calloc(1, sizeof(sb_async_job));
|
|
600
|
+
if (!j) abort();
|
|
601
|
+
j->id = id;
|
|
602
|
+
j->stream = stream;
|
|
603
|
+
j->fd = -1;
|
|
604
|
+
size_t req_len = 0;
|
|
605
|
+
char* req = sb_v1_request(json, json_len, body, body_len, &req_len);
|
|
606
|
+
if (!req) abort();
|
|
607
|
+
j->out = (unsigned char*)malloc(4 + req_len);
|
|
608
|
+
if (!j->out) abort();
|
|
609
|
+
for (int k = 0; k < 4; k++) j->out[k] = (unsigned char)((req_len >> (8 * k)) & 0xff);
|
|
610
|
+
memcpy(j->out + 4, req, req_len);
|
|
611
|
+
free(req);
|
|
612
|
+
j->out_len = 4 + req_len;
|
|
613
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
614
|
+
int start = !sb_io_started;
|
|
615
|
+
if (start) {
|
|
616
|
+
signal(SIGPIPE, SIG_IGN); // a dead broker must yield EPIPE, not kill the sprout
|
|
617
|
+
if (pipe(sb_io_wake) != 0) abort();
|
|
618
|
+
sb_io_nonblock(sb_io_wake[0]);
|
|
619
|
+
sb_io_nonblock(sb_io_wake[1]);
|
|
620
|
+
sb_io_started = 1;
|
|
621
|
+
}
|
|
622
|
+
j->next = sb_async_queue;
|
|
623
|
+
sb_async_queue = j;
|
|
624
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
625
|
+
if (start) {
|
|
626
|
+
pthread_attr_t attr;
|
|
627
|
+
pthread_attr_init(&attr);
|
|
628
|
+
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
|
629
|
+
pthread_attr_setstacksize(&attr, 256 * 1024);
|
|
630
|
+
pthread_t thread;
|
|
631
|
+
int failed = pthread_create(&thread, &attr, sb_io_thread, 0);
|
|
632
|
+
pthread_attr_destroy(&attr);
|
|
633
|
+
// No thread (a pids limit): every async call fails; the sync path still works.
|
|
634
|
+
if (failed) {
|
|
635
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
636
|
+
sb_async_job* orphans = sb_async_queue;
|
|
637
|
+
sb_async_queue = 0;
|
|
638
|
+
sb_io_started = 0;
|
|
639
|
+
close(sb_io_wake[0]); close(sb_io_wake[1]);
|
|
640
|
+
sb_io_wake[0] = sb_io_wake[1] = -1;
|
|
641
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
642
|
+
while (orphans) {
|
|
643
|
+
sb_async_job* q = orphans;
|
|
644
|
+
orphans = q->next;
|
|
645
|
+
sb_async_error(q, -7);
|
|
646
|
+
sb_async_finish(q);
|
|
647
|
+
}
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
char one = 1;
|
|
652
|
+
while (write(sb_io_wake[1], &one, 1) < 0 && errno == EINTR) {}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// The caller gave up on exchange id: the I/O thread drops it and closes its
|
|
656
|
+
// connection. One that already finished is dropped by the drain instead, since its
|
|
657
|
+
// placeholder timer is gone.
|
|
658
|
+
void sb_async_cancel(u32 id) {
|
|
659
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
660
|
+
int started = sb_io_started;
|
|
661
|
+
if (started) {
|
|
662
|
+
if (sb_async_cancels_n == sb_async_cancels_cap) {
|
|
663
|
+
sb_async_cancels_cap = sb_async_cancels_cap ? sb_async_cancels_cap * 2 : 8;
|
|
664
|
+
sb_async_cancels = (u32*)realloc(sb_async_cancels, sb_async_cancels_cap * sizeof(u32));
|
|
665
|
+
if (!sb_async_cancels) abort();
|
|
666
|
+
}
|
|
667
|
+
sb_async_cancels[sb_async_cancels_n++] = id;
|
|
668
|
+
}
|
|
669
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
670
|
+
if (!started) return;
|
|
671
|
+
char one = 1;
|
|
672
|
+
while (write(sb_io_wake[1], &one, 1) < 0 && errno == EINTR) {}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const char* sb_async_reply_json(u32 id, size_t* len) {
|
|
676
|
+
if (!sb_async_current || sb_async_current->id != id) return 0;
|
|
677
|
+
*len = sb_async_current->json_len;
|
|
678
|
+
return sb_async_current->json;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Host, on its loop: fires the callback for each reply frame, oldest first. Its
|
|
682
|
+
// bytes go where __sbTakeBin reads them. A final frame also drops the placeholder
|
|
683
|
+
// timer; an earlier frame of a streamed response leaves it for the next one.
|
|
684
|
+
void sb_async_drain(void) {
|
|
685
|
+
pthread_mutex_lock(&sb_async_lock);
|
|
686
|
+
sb_async_delivery* ready = sb_async_ready;
|
|
687
|
+
sb_async_ready = 0;
|
|
688
|
+
pthread_mutex_unlock(&sb_async_lock);
|
|
689
|
+
sb_async_delivery* oldest = 0;
|
|
690
|
+
while (ready) {
|
|
691
|
+
sb_async_delivery* next = ready->next;
|
|
692
|
+
ready->next = oldest;
|
|
693
|
+
oldest = ready;
|
|
694
|
+
ready = next;
|
|
695
|
+
}
|
|
696
|
+
while (oldest) {
|
|
697
|
+
sb_async_delivery* d = oldest;
|
|
698
|
+
oldest = d->next;
|
|
699
|
+
if (d->final) porf_native_fetch_timer_clear(d->id);
|
|
700
|
+
if (sb_bin_reply) free(sb_bin_reply);
|
|
701
|
+
sb_bin_reply = d->bin;
|
|
702
|
+
sb_bin_reply_len = d->bin_len;
|
|
703
|
+
d->bin = 0;
|
|
704
|
+
sb_async_current = d; // read by __sbHttpAsyncResult in the callback
|
|
705
|
+
porf_native_fetch_fire_timer(d->id);
|
|
706
|
+
sb_async_current = 0;
|
|
707
|
+
free(d->json);
|
|
708
|
+
free(d);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
265
711
|
`;
|
|
266
712
|
|
|
267
713
|
// One request string in, one reply string out. `reqJson` is a parameter, so the
|
|
@@ -400,6 +846,12 @@ globalThis.__sbR2MultipartPut = function (bucket, key, uploadId, partNumber, bod
|
|
|
400
846
|
globalThis.__sbFetchUpstream = function (op, msg) {
|
|
401
847
|
const req = { v: 1, id: ++__sbReqId, token: __sbEnv("SB_BROKER_TOKEN"), op };
|
|
402
848
|
for (const k in msg) req[k] = msg[k];
|
|
849
|
+
// Queued (async host); the reply reaches the callback registered under asyncId, in
|
|
850
|
+
// several frames when msg.stream asks for a streamed response.
|
|
851
|
+
if (msg.asyncId > 0) {
|
|
852
|
+
__sbCallAsync(JSON.stringify(req), "", msg.asyncId, msg.stream === true ? "1" : "0");
|
|
853
|
+
return { ok: true, pending: true };
|
|
854
|
+
}
|
|
403
855
|
const reply = JSON.parse(__sbCallBin(JSON.stringify(req), ""));
|
|
404
856
|
if (reply.ok === false) throw new Error("sproutboat " + op + ": " + (reply.error || "failed"));
|
|
405
857
|
const bytes = __sbTakeBin();
|
|
@@ -407,16 +859,74 @@ globalThis.__sbFetchUpstream = function (op, msg) {
|
|
|
407
859
|
return reply;
|
|
408
860
|
};
|
|
409
861
|
|
|
410
|
-
//
|
|
411
|
-
//
|
|
862
|
+
// Queues one v1 exchange for the I/O thread, under timer id `asyncId`. `stream` is "1"
|
|
863
|
+
// when the reply may come as several frames, each firing the callback; a string, like
|
|
864
|
+
// the embedded transport's flags, so the C below reads it the same way whatever type
|
|
865
|
+
// Porffor infers.
|
|
866
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
867
|
+
function __sbCallAsync(reqJson, body, asyncId, stream) {
|
|
868
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
869
|
+
Porffor.c`
|
|
870
|
+
void sb_async_submit_v1(u32, int, const char*, size_t, const char*, size_t);
|
|
871
|
+
const char* __j; size_t __jl; char* __jo = 0;
|
|
872
|
+
porf_native_fetch_read_value(reqJson, &__j, &__jl, &__jo);
|
|
873
|
+
const char* __b; size_t __bl; char* __bo = 0;
|
|
874
|
+
if (porf_native_fetch_read_raw_bytes(body, &__b, &__bl) != 0) {
|
|
875
|
+
porf_native_fetch_read_value(body, &__b, &__bl, &__bo);
|
|
876
|
+
}
|
|
877
|
+
const char* __s; size_t __sl; char* __so = 0;
|
|
878
|
+
porf_native_fetch_read_value(stream, &__s, &__sl, &__so);
|
|
879
|
+
const int __stream = __sl > 0 && __s[0] == '1';
|
|
880
|
+
if (__so) free(__so);
|
|
881
|
+
if (asyncId.type == 1 && asyncId.val > 0) sb_async_submit_v1((u32)asyncId.val, __stream, __j, __jl, __b, __bl);
|
|
882
|
+
if (__jo) free(__jo);
|
|
883
|
+
if (__bo) free(__bo);
|
|
884
|
+
`;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// The caller gave up on the exchange under timer id `asyncId`: its connection
|
|
888
|
+
// closes, which the broker reads as "stop" and passes on.
|
|
889
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
890
|
+
function __sbCancelAsync(asyncId) {
|
|
891
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
892
|
+
Porffor.c`
|
|
893
|
+
void sb_async_cancel(u32);
|
|
894
|
+
if (asyncId.type == 1 && asyncId.val > 0) sb_async_cancel((u32)asyncId.val);
|
|
895
|
+
`;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Whether the host runs exchanges off its loop (it installed a waker).
|
|
412
899
|
function __sbHttpAsyncEnabled() {
|
|
413
|
-
|
|
900
|
+
let res = false;
|
|
901
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
902
|
+
Porffor.c`
|
|
903
|
+
int sb_async_enabled(void);
|
|
904
|
+
if (sb_async_enabled()) res = porf_box(1, 2);
|
|
905
|
+
`;
|
|
906
|
+
return res;
|
|
414
907
|
}
|
|
415
|
-
|
|
416
|
-
|
|
908
|
+
|
|
909
|
+
// The reply JSON of the async exchange now firing its callback, or "".
|
|
910
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
911
|
+
function __sbHttpAsyncResult(asyncId) {
|
|
912
|
+
let res = "";
|
|
913
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
914
|
+
Porffor.c`
|
|
915
|
+
const char* sb_async_reply_json(u32, size_t*);
|
|
916
|
+
size_t __n = 0;
|
|
917
|
+
const char* __r = asyncId.type == 1 ? sb_async_reply_json((u32)asyncId.val, &__n) : 0;
|
|
918
|
+
if (__r) res = porf_box((f64)porf_native_fetch_alloc_bytestring(__r, __n), 195);
|
|
919
|
+
`;
|
|
920
|
+
return res;
|
|
417
921
|
}
|
|
418
|
-
|
|
419
|
-
|
|
922
|
+
|
|
923
|
+
// An async exchange's reply JSON -> the fetch op's result, its bytes as the body.
|
|
924
|
+
function __sbFetchReply(raw) {
|
|
925
|
+
const reply = JSON.parse(raw);
|
|
926
|
+
if (reply.ok === false) throw new Error(reply.error || "failed");
|
|
927
|
+
const bytes = __sbTakeBin();
|
|
928
|
+
reply.body = reply.body != null ? __sbToBytes(reply.body) : bytes;
|
|
929
|
+
return reply;
|
|
420
930
|
}
|
|
421
931
|
|
|
422
932
|
/** Static asset metadata stays JSON; the response body uses the v1 byte tail. */
|