cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
durable_queue.c
Go to the documentation of this file.
1
25#define _POSIX_C_SOURCE 200809L
28#ifdef CWIST_NATS
30#endif
32#ifdef CWIST_NATS
33#include <nats.h>
34#endif
35#include <stdio.h>
36#include <stdlib.h>
37#include <string.h>
38#include <time.h>
39#include <unistd.h>
40
41#define CWIST_JQ_GROUP "cwist-jq"
42#define CWIST_JQ_DEFAULT_VISIBILITY_MS 30000u
43#define CWIST_JQ_DEFAULT_MAX_RETRIES 5u
44#define CWIST_JQ_MAX_POLL_SLICE_MS 200u /* max single XREADGROUP BLOCK */
45
46/* Atomically schedules a nacked job: XACK, then either park it in the
47 * delay zset or dead-letter it. Returns 1 (delayed), 2 (dead-lettered),
48 * 0 (entry no longer pending). */
49static const char *JQ_NACK_SCRIPT = "local ok = redis.call('XACK', KEYS[1], KEYS[2], ARGV[1])\n"
50 "if ok == 0 then return 0 end\n"
51 "redis.call('HDEL', KEYS[5], ARGV[1])\n"
52 "if tonumber(ARGV[2]) > tonumber(ARGV[3]) then\n"
53 " redis.call('RPUSH', KEYS[3], ARGV[4])\n"
54 " return 2\n"
55 "end\n"
56 "redis.call('ZADD', KEYS[4], ARGV[5], ARGV[4])\n"
57 "return 1\n";
58
59/* Moves due delayed blobs back onto the stream. The attempts hash is
60 * pre-seeded with attempts-1 because the fresh-claim path HINCRBYs. */
61static const char *JQ_REAP_SCRIPT =
62 "local items = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], "
63 "'LIMIT', 0, 128)\n"
64 "for _, blob in ipairs(items) do\n"
65 " local tl = string.byte(blob,1)*16777216 + string.byte(blob,2)*65536 + "
66 "string.byte(blob,3)*256 + string.byte(blob,4)\n"
67 " local at = string.byte(blob,5)*16777216 + string.byte(blob,6)*65536 + "
68 "string.byte(blob,7)*256 + string.byte(blob,8)\n"
69 " local id = redis.call('XADD', KEYS[2], '*', 'type', "
70 "string.sub(blob, 9, 8 + tl), 'payload', string.sub(blob, 9 + tl))\n"
71 " redis.call('HSET', KEYS[3], id, at - 1)\n"
72 " redis.call('ZREM', KEYS[1], blob)\n"
73 "end\n"
74 "return #items\n";
75
76/* Ensures the consumer group exists; 1 = created, 2 = already existed,
77 * 0 = real error. */
78static const char *JQ_GROUP_SCRIPT =
79 "local ok, err = pcall(redis.call, 'XGROUP', 'CREATE', KEYS[1], ARGV[1], "
80 "'$', 'MKSTREAM')\n"
81 "if ok then return 1 end\n"
82 "if string.match(err, 'BUSYGROUP') then return 2 end\n"
83 "return 0\n";
84
86
87#ifdef CWIST_NATS
89#endif
90
91typedef struct {
92 cwist_redis_t *conn; /* borrowed */
93 char *stream;
94 char *attempts;
95 char *dead;
96 char *delay;
97 char *consumer; /* owned copy */
98} jq_redis_t;
99
100#ifdef CWIST_NATS
101typedef struct {
102 natsConnection *conn; /* borrowed */
103 jsCtx *js;
104 natsSubscription *sub;
105 char *subject; /* <base>.jobs */
106 char *dead_subject; /* <base>.dead */
107 char *dead_stream; /* <base>_DEAD */
108} jq_nats_t;
109#endif
110
111struct cwist_job_queue {
112 int backend; /* 0 = redis, 1 = nats */
113 char *name;
114 char *consumer;
115 uint64_t visibility_timeout_ms;
116 uint32_t max_retries;
117 uint64_t retry_delay_ms;
118 jq_dead_count_fn dead_count;
119 union {
120 jq_redis_t redis;
121#ifdef CWIST_NATS
123#endif
124 } u;
125};
126
127struct cwist_job {
129 void *backend; /* natsMsg* for the NATS backend, NULL for redis */
130 char *id;
131 char *type; /* NULL or owned copy */
132 unsigned char *payload;
133 size_t payload_len;
134 uint32_t attempts;
135};
136
138static cwist_error_t jq_err(int16_t code) {
140 e.error.err_i16 = code;
141 return e;
142}
143
145static uint64_t jq_now_ms(void) {
146 struct timespec ts;
148 return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
149}
150
151static unsigned int jq_consumer_seq = 0;
152
154static char *jq_default_consumer(void) {
155 char buf[64];
156 snprintf(buf, sizeof(buf), "w%d-%u", (int)getpid(), ++jq_consumer_seq);
157 return cwist_strdup(buf);
158}
159
161static bool jq_opts_init(cwist_job_queue_t *q, const cwist_job_queue_opts_t *opts) {
162 if (!opts || !opts->name || !*opts->name) return false;
163 q->name = cwist_strdup(opts->name);
164 q->consumer =
165 opts->consumer && *opts->consumer ? cwist_strdup(opts->consumer) : jq_default_consumer();
166 q->visibility_timeout_ms =
168 q->max_retries = opts->max_retries ? opts->max_retries : CWIST_JQ_DEFAULT_MAX_RETRIES;
169 q->retry_delay_ms = opts->retry_delay_ms;
170 if (!q->name || !q->consumer) return false;
171 return true;
172}
173
179static void cwist_job_free(cwist_job_t *job) {
180 if (!job) return;
181#ifdef CWIST_NATS
182 if (job->backend) natsMsg_Destroy((natsMsg *)job->backend);
183#endif
184 cwist_free(job->id);
185 cwist_free(job->type);
186 cwist_free(job->payload);
188}
189
192 cwist_job_free(job);
193}
194
196const void *cwist_job_payload(const cwist_job_t *job, size_t *len) {
197 if (!job) {
198 if (len) *len = 0;
199 return NULL;
200 }
201 if (len) *len = job->payload_len;
202 return job->payload;
203}
204
206const char *cwist_job_type(const cwist_job_t *job) {
207 return job ? job->type : NULL;
208}
209
211const char *cwist_job_id(const cwist_job_t *job) {
212 return job ? job->id : NULL;
213}
214
217 return job ? job->attempts : 0;
218}
219
220/* --- Redis backend ------------------------------------------------------ */
221
223static char *jq_key(const char *name, const char *suffix) {
224 size_t n = strlen(name) + strlen(suffix) + 16;
225 char *key = cwist_alloc(n);
226 if (key) snprintf(key, n, "cwist:jq:%s:%s", name, suffix);
227 return key;
228}
229
235static cwist_error_t jq_redis_cmd(cwist_redis_t *conn, size_t argc, const void *const *argv,
236 const size_t *argv_lens, cwist_redis_reply_t **reply) {
238 if (!cwist_error_is_ok(&err) || !*reply) {
240 *reply = NULL;
241 return jq_err(CWIST_FAILURE);
242 }
243 return jq_err(0);
244}
245
247static cwist_error_t jq_redis_int(cwist_redis_t *conn, size_t argc, const void *const *argv,
248 const size_t *argv_lens, long long *out) {
250 cwist_error_t err = jq_redis_cmd(conn, argc, argv, argv_lens, &reply);
251 if (!cwist_error_is_ok(&err)) return err;
252 if (reply->type == ':' || reply->type == '$')
253 *out = reply->integer ? reply->integer : strtoll(reply->str ? reply->str : "0", NULL, 10);
254 else
255 *out = 0;
257 return jq_err(0);
258}
259
261static char *jq_blob(const char *type, uint32_t attempts, const void *payload, size_t payload_len,
262 size_t *out_len) {
263 uint32_t type_len = type ? (uint32_t)strlen(type) : 0;
264 size_t len = 8 + type_len + payload_len;
265 char *blob = cwist_alloc(len ? len : 1);
266 if (!blob) return NULL;
267 blob[0] = (char)(type_len >> 24);
268 blob[1] = (char)(type_len >> 16);
269 blob[2] = (char)(type_len >> 8);
270 blob[3] = (char)type_len;
271 blob[4] = (char)(attempts >> 24);
272 blob[5] = (char)(attempts >> 16);
273 blob[6] = (char)(attempts >> 8);
274 blob[7] = (char)attempts;
275 if (type_len) memcpy(blob + 8, type, type_len);
276 if (payload_len) memcpy(blob + 8 + type_len, payload, payload_len);
277 *out_len = len;
278 return blob;
279}
280
285static cwist_error_t jq_redis_enqueue(cwist_job_queue_t *q, const void *payload, size_t payload_len,
286 const char *type) {
287 jq_redis_t *rs = &q->u.redis;
288 const char empty[] = "";
289 if (!type) type = empty;
290 const void *argv[] = {"XADD", rs->stream, "*", "type", type, "payload", payload};
291 size_t lens[] = {4, strlen(rs->stream), 1, 4, strlen(type), 7, payload_len};
293 cwist_error_t err = jq_redis_cmd(rs->conn, 7, argv, lens, &reply);
295 return err;
296}
297
299static const cwist_redis_reply_t *jq_entry_field(const cwist_redis_reply_t *fields,
300 const char *name) {
301 if (!fields || fields->type != '*') return NULL;
302 for (size_t i = 0; i + 1 < fields->elements; i += 2) {
303 const cwist_redis_reply_t *k = fields->element[i];
304 if (k && k->str && strcmp(k->str, name) == 0) return fields->element[i + 1];
305 }
306 return NULL;
307}
308
313static cwist_job_t *jq_job_from_entry(cwist_job_queue_t *q, const cwist_redis_reply_t *entry) {
314 if (!entry || entry->type != '*' || entry->elements != 2 || !entry->element[0] ||
315 !entry->element[0]->str)
316 return NULL;
317 const cwist_redis_reply_t *id = entry->element[0];
318 const cwist_redis_reply_t *fields = entry->element[1];
319 const cwist_redis_reply_t *type = jq_entry_field(fields, "type");
320 const cwist_redis_reply_t *payload = jq_entry_field(fields, "payload");
321
322 cwist_job_t *job = cwist_alloc(sizeof(*job));
323 if (!job) return NULL;
324 memset(job, 0, sizeof(*job));
325 job->q = q;
326 job->id = cwist_strndup(id->str, id->len);
327 if (type && type->str && type->len) job->type = cwist_strndup(type->str, type->len);
328 job->payload_len = payload && payload->str ? payload->len : 0;
329 if (job->payload_len) {
330 job->payload = cwist_alloc(job->payload_len);
331 if (job->payload) memcpy(job->payload, payload->str, job->payload_len);
332 }
333 if (!job->id || (job->payload_len && !job->payload)) {
334 cwist_job_free(job);
335 return NULL;
336 }
337 return job;
338}
339
341static cwist_error_t jq_redis_delivery_count(cwist_job_queue_t *q, const char *id, size_t id_len,
343 jq_redis_t *rs = &q->u.redis;
344 char one[] = "1";
345 const void *argv[] = {"HINCRBY", rs->attempts, id, one};
346 size_t lens[] = {7, strlen(rs->attempts), id_len, 1};
347 long long n = 0;
348 cwist_error_t err = jq_redis_int(rs->conn, 4, argv, lens, &n);
349 if (cwist_error_is_ok(&err)) *out_attempts = n > 0 ? (uint32_t)n : 1;
350 return err;
351}
352
354static bool jq_redis_run(cwist_redis_t *conn, size_t argc, const void *const *argv,
355 const size_t *argv_lens) {
357 cwist_error_t err = jq_redis_cmd(conn, argc, argv, argv_lens, &reply);
359 return cwist_error_is_ok(&err);
360}
361
363static void jq_redis_dead_letter(cwist_job_queue_t *q, const char *id, size_t id_len,
364 const char *type, const void *payload, size_t payload_len,
365 uint32_t attempts) {
366 jq_redis_t *rs = &q->u.redis;
367 size_t blob_len = 0;
368 char *blob = jq_blob(type, attempts, payload, payload_len, &blob_len);
369 if (blob) {
370 const void *argv[] = {"RPUSH", rs->dead, blob};
371 size_t lens[] = {5, strlen(rs->dead), blob_len};
372 jq_redis_run(rs->conn, 3, argv, lens);
373 cwist_free(blob);
374 }
375 const void *argv[] = {"XACK", rs->stream, CWIST_JQ_GROUP, id};
376 size_t lens[] = {4, strlen(rs->stream), strlen(CWIST_JQ_GROUP), id_len};
377 jq_redis_run(rs->conn, 4, argv, lens);
378 const void *dargv[] = {"HDEL", rs->attempts, id};
379 size_t dlens[] = {4, strlen(rs->attempts), id_len};
380 jq_redis_run(rs->conn, 3, dargv, dlens);
381}
382
384static cwist_job_t *jq_redis_autoclaim(cwist_job_queue_t *q) {
385 jq_redis_t *rs = &q->u.redis;
386 char vis[32];
387 int vis_len = snprintf(vis, sizeof(vis), "%llu", (unsigned long long)q->visibility_timeout_ms);
388 const char *zero[] = {"0-0"};
389 const char *count[] = {"128"};
390 const void *argv[] = {"XAUTOCLAIM", rs->stream, CWIST_JQ_GROUP, rs->consumer,
391 vis, zero[0], "COUNT", count[0]};
392 size_t lens[] = {
393 10, strlen(rs->stream), strlen(CWIST_JQ_GROUP), strlen(rs->consumer), (size_t)vis_len, 3, 5,
394 3};
396 cwist_error_t err = jq_redis_cmd(rs->conn, 8, argv, lens, &reply);
397 if (!cwist_error_is_ok(&err) || !reply || reply->type != '*' || reply->elements < 2) {
399 return NULL;
400 }
402 const cwist_redis_reply_t *entries = reply->element[1];
403 if (entries && entries->type == '*') {
404 for (size_t i = 0; i < entries->elements && !job; i++) {
405 cwist_job_t *candidate = jq_job_from_entry(q, entries->element[i]);
406 if (!candidate) continue;
407 uint32_t attempts = 0;
409 jq_redis_delivery_count(q, candidate->id, strlen(candidate->id), &attempts);
410 if (!cwist_error_is_ok(&derr) || attempts <= q->max_retries) {
411 candidate->attempts = attempts;
412 job = candidate;
413 } else {
414 jq_redis_dead_letter(q, candidate->id, strlen(candidate->id), candidate->type,
415 candidate->payload, candidate->payload_len, attempts);
416 cwist_job_free(candidate);
417 }
418 }
419 }
421 return job;
422}
423
425static cwist_job_t *jq_redis_readgroup(cwist_job_queue_t *q, uint64_t block_ms) {
426 jq_redis_t *rs = &q->u.redis;
427 char block[32];
428 int block_len = snprintf(block, sizeof(block), "%llu", (unsigned long long)block_ms);
429 const char gt[] = ">";
430 const void *argv[] = {"XREADGROUP", "GROUP", CWIST_JQ_GROUP, rs->consumer, "COUNT", "1",
431 "BLOCK", block, "STREAMS", rs->stream, gt};
432 size_t lens[] = {10,
433 5,
435 strlen(rs->consumer),
436 5,
437 1,
438 5,
440 7,
441 strlen(rs->stream),
442 1};
444 cwist_error_t err = jq_redis_cmd(rs->conn, 11, argv, lens, &reply);
445 if (!cwist_error_is_ok(&err) || !reply || reply->type != '*' || reply->elements < 1) {
447 return NULL;
448 }
451 if (top && top->type == '*' && top->elements == 2 && top->element[1] &&
452 top->element[1]->type == '*') {
453 const cwist_redis_reply_t *entries = top->element[1];
454 for (size_t i = 0; i < entries->elements && !job; i++) {
455 cwist_job_t *candidate = jq_job_from_entry(q, entries->element[i]);
456 if (!candidate) continue;
457 uint32_t attempts = 0;
459 jq_redis_delivery_count(q, candidate->id, strlen(candidate->id), &attempts);
460 if (cwist_error_is_ok(&derr)) {
461 candidate->attempts = attempts;
462 job = candidate;
463 } else {
464 cwist_job_free(candidate);
465 }
466 }
467 }
469 return job;
470}
471
473static void jq_redis_reap_delay(cwist_job_queue_t *q) {
474 jq_redis_t *rs = &q->u.redis;
475 char now[32];
476 int now_len = snprintf(now, sizeof(now), "%llu", (unsigned long long)jq_now_ms());
477 const char three[] = "3";
478 const void *argv[] = {"EVAL", JQ_REAP_SCRIPT, three, rs->delay, rs->stream, rs->attempts, now};
479 size_t lens[] = {4,
480 strlen(JQ_REAP_SCRIPT),
481 1,
482 strlen(rs->delay),
483 strlen(rs->stream),
484 strlen(rs->attempts),
485 (size_t)now_len};
486 jq_redis_run(rs->conn, 7, argv, lens);
487}
488
495static cwist_error_t jq_redis_claim(cwist_job_queue_t *q, uint64_t timeout_ms, cwist_job_t **job) {
496 uint64_t deadline = jq_now_ms() + timeout_ms;
497 bool polled = false;
498 for (;;) {
499 jq_redis_reap_delay(q);
500 cwist_job_t *found = jq_redis_autoclaim(q);
501 if (found) {
502 *job = found;
503 return jq_err(0);
504 }
505 uint64_t now = jq_now_ms();
506 if (polled && now >= deadline) return jq_err(CWIST_ERROR_TIMEOUT);
507 polled = true;
508 uint64_t block = deadline > now ? deadline - now : 1;
510 found = jq_redis_readgroup(q, block);
511 if (found) {
512 *job = found;
513 return jq_err(0);
514 }
515 if (jq_now_ms() >= deadline) return jq_err(CWIST_ERROR_TIMEOUT);
516 }
517}
518
520static cwist_error_t jq_redis_ack(cwist_job_queue_t *q, cwist_job_t *job) {
521 jq_redis_t *rs = &q->u.redis;
522 const void *argv[] = {"XACK", rs->stream, CWIST_JQ_GROUP, job->id};
523 size_t lens[] = {4, strlen(rs->stream), strlen(CWIST_JQ_GROUP), strlen(job->id)};
525 cwist_error_t err = jq_redis_cmd(rs->conn, 4, argv, lens, &reply);
527 const void *dargv[] = {"HDEL", rs->attempts, job->id};
528 size_t dlens[] = {4, strlen(rs->attempts), strlen(job->id)};
529 jq_redis_run(rs->conn, 3, dargv, dlens);
530 return err;
531}
532
539static cwist_error_t jq_redis_nack(cwist_job_queue_t *q, cwist_job_t *job) {
540 jq_redis_t *rs = &q->u.redis;
541 uint32_t next = job->attempts + 1;
542 uint64_t due = jq_now_ms() + q->retry_delay_ms;
543 char next_buf[16], max_buf[16], due_buf[32];
544 int next_len = snprintf(next_buf, sizeof(next_buf), "%u", next);
545 int max_len = snprintf(max_buf, sizeof(max_buf), "%u", q->max_retries);
546 int due_len = snprintf(due_buf, sizeof(due_buf), "%llu", (unsigned long long)due);
547 size_t blob_len = 0;
548 char *blob = jq_blob(job->type, next, job->payload, job->payload_len, &blob_len);
549 if (!blob) return jq_err(CWIST_ERROR_NOMEM);
550 const char five[] = "5";
551 const void *argv[] = {"EVAL", JQ_NACK_SCRIPT, five, rs->stream, CWIST_JQ_GROUP,
552 rs->dead, rs->delay, rs->attempts, job->id, next_buf,
553 max_buf, blob, due_buf};
554 size_t lens[] = {4,
555 strlen(JQ_NACK_SCRIPT),
556 1,
557 strlen(rs->stream),
559 strlen(rs->dead),
560 strlen(rs->delay),
561 strlen(rs->attempts),
562 strlen(job->id),
564 (size_t)max_len,
565 blob_len,
566 (size_t)due_len};
568 cwist_error_t err = jq_redis_cmd(rs->conn, 13, argv, lens, &reply);
570 cwist_free(blob);
571 return err;
572}
573
575static cwist_error_t jq_redis_dead_count(cwist_job_queue_t *q, uint64_t *out_count) {
576 jq_redis_t *rs = &q->u.redis;
577 const void *argv[] = {"LLEN", rs->dead};
578 size_t lens[] = {4, strlen(rs->dead)};
579 long long n = 0;
580 cwist_error_t err = jq_redis_int(rs->conn, 2, argv, lens, &n);
581 if (cwist_error_is_ok(&err)) *out_count = n > 0 ? (uint64_t)n : 0;
582 return err;
583}
584
592 const cwist_job_queue_opts_t *opts) {
593 if (!conn) return NULL;
594 cwist_job_queue_t *q = cwist_alloc(sizeof(*q));
595 if (!q) return NULL;
596 memset(q, 0, sizeof(*q));
597 if (!jq_opts_init(q, opts)) goto fail;
598 q->backend = 0;
599 q->dead_count = jq_redis_dead_count;
600 jq_redis_t *rs = &q->u.redis;
601 rs->conn = conn;
602 rs->stream = jq_key(q->name, "s");
603 rs->attempts = jq_key(q->name, "a");
604 rs->dead = jq_key(q->name, "dead");
605 rs->delay = jq_key(q->name, "delay");
606 rs->consumer = cwist_strdup(q->consumer);
607 if (!rs->stream || !rs->attempts || !rs->dead || !rs->delay || !rs->consumer) goto fail;
608
609 const void *argv[] = {"EVAL", JQ_GROUP_SCRIPT, "1", rs->stream, CWIST_JQ_GROUP};
610 size_t lens[] = {4, strlen(JQ_GROUP_SCRIPT), 1, strlen(rs->stream), strlen(CWIST_JQ_GROUP)};
611 long long created = 0;
612 cwist_error_t gerr = jq_redis_int(conn, 5, argv, lens, &created);
613 if (!cwist_error_is_ok(&gerr) || created == 0) goto fail;
614 return q;
615
616fail:
617 if (q) {
618 if (q->u.redis.stream) cwist_free(q->u.redis.stream);
619 if (q->u.redis.attempts) cwist_free(q->u.redis.attempts);
620 if (q->u.redis.dead) cwist_free(q->u.redis.dead);
621 if (q->u.redis.delay) cwist_free(q->u.redis.delay);
622 if (q->u.redis.consumer) cwist_free(q->u.redis.consumer);
623 cwist_free(q->name);
624 cwist_free(q->consumer);
625 cwist_free(q);
626 }
627 return NULL;
628}
629
630/* --- NATS backend ------------------------------------------------------- */
631
632#ifdef CWIST_NATS
635static char *jq_sanitize(const char *name) {
636 size_t len = strlen(name);
637 char *out = cwist_alloc(len + 1);
638 if (!out) return NULL;
639 for (size_t i = 0; i < len; i++) {
640 char c = name[i];
641 out[i] = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
642 c == '-' || c == '_'
643 ? c
644 : '_';
645 }
646 out[len] = '\0';
647 return out;
648}
649
654static cwist_error_t jq_nats_ensure_stream(jq_nats_t *ns, const char *stream, const char *subject) {
655 jsErrCode jerr = 0;
657 if (js_GetStreamInfo(&si, ns->js, stream, NULL, &jerr) == NATS_OK) {
659 return jq_err(0);
660 }
661 const char *subjects[] = {subject};
662 jsStreamConfig cfg;
663 memset(&cfg, 0, sizeof(cfg));
664 cfg.Name = stream;
665 cfg.Subjects = subjects;
666 cfg.SubjectsLen = 1;
667 cfg.Storage = js_FileStorage;
668 if (js_AddStream(&si, ns->js, &cfg, NULL, &jerr) != NATS_OK) return jq_err(CWIST_FAILURE);
670 return jq_err(0);
671}
672
680 const cwist_job_queue_opts_t *opts) {
681 if (!nats) return NULL;
683 if (!nc) return NULL;
684 cwist_job_queue_t *q = cwist_alloc(sizeof(*q));
685 if (!q) return NULL;
686 memset(q, 0, sizeof(*q));
687 if (!jq_opts_init(q, opts)) goto fail;
688 q->backend = 1;
689 q->dead_count = jq_nats_dead_count;
690 jq_nats_t *ns = &q->u.nats;
691 ns->conn = nc;
692 char *base = jq_sanitize(q->name);
693 char *consumer = jq_sanitize(q->consumer);
694 if (!base || !consumer) {
695 cwist_free(base);
696 cwist_free(consumer);
697 goto fail;
698 }
699 size_t n = strlen(base);
700 ns->subject = cwist_alloc(n + 6);
701 ns->dead_subject = cwist_alloc(n + 6);
702 ns->dead_stream = cwist_alloc(n + 6);
703 if (!ns->subject || !ns->dead_subject || !ns->dead_stream) {
704 cwist_free(base);
705 cwist_free(consumer);
706 goto fail;
707 }
708 snprintf(ns->subject, n + 6, "%s.jobs", base);
709 snprintf(ns->dead_subject, n + 6, "%s.dead", base);
710 snprintf(ns->dead_stream, n + 5, "%s_DEAD", base);
711
712 if (natsConnection_JetStream(&ns->js, nc, NULL) != NATS_OK) {
713 cwist_free(base);
714 cwist_free(consumer);
715 goto fail;
716 }
717 cwist_error_t serr = jq_nats_ensure_stream(ns, base, ns->subject);
718 if (cwist_error_is_ok(&serr))
719 serr = jq_nats_ensure_stream(ns, ns->dead_stream, ns->dead_subject);
720 if (!cwist_error_is_ok(&serr)) {
721 cwist_free(base);
722 cwist_free(consumer);
723 goto fail;
724 }
725
726 /* Durable pull consumer; tolerate "already exists" (same config from a
727 * previous run). */
728 jsErrCode jerr = 0;
731 memset(&cc, 0, sizeof(cc));
732 cc.Durable = consumer;
733 cc.AckPolicy = js_AckExplicit;
734 cc.AckWait = (int64_t)q->visibility_timeout_ms * 1000000;
735 cc.MaxDeliver = (int64_t)q->max_retries + 1;
736 cc.DeliverPolicy = js_DeliverAll;
737 cc.FilterSubject = ns->subject;
738 if (js_AddConsumer(&ci, ns->js, base, &cc, NULL, &jerr) == NATS_OK) {
740 } else if (js_GetConsumerInfo(&ci, ns->js, base, consumer, NULL, &jerr) != NATS_OK) {
741 cwist_free(base);
742 cwist_free(consumer);
743 goto fail;
744 } else {
746 }
747 if (js_PullSubscribe(&ns->sub, ns->js, ns->subject, consumer, NULL, NULL, &jerr) != NATS_OK ||
749 cwist_free(base);
750 cwist_free(consumer);
751 goto fail;
752 }
753 cwist_free(base);
754 cwist_free(consumer);
755 return q;
756
757fail:
758 if (q) {
759 if (q->u.nats.sub) natsSubscription_Destroy(q->u.nats.sub);
760 if (q->u.nats.js) jsCtx_Destroy(q->u.nats.js);
761 cwist_free(q->u.nats.subject);
762 cwist_free(q->u.nats.dead_subject);
763 cwist_free(q->u.nats.dead_stream);
764 cwist_free(q->name);
765 cwist_free(q->consumer);
766 cwist_free(q);
767 }
768 return NULL;
769}
770
775static cwist_error_t jq_nats_enqueue(cwist_job_queue_t *q, const void *payload, size_t payload_len,
776 const char *type) {
777 jq_nats_t *ns = &q->u.nats;
778 natsMsg *msg = NULL;
779 if (natsMsg_Create(&msg, ns->subject, NULL, (const char *)payload, (int)payload_len) != NATS_OK)
780 return jq_err(CWIST_FAILURE);
781 if (type && natsMsgHeader_Set(msg, "Cwist-Type", type) != NATS_OK) {
783 return jq_err(CWIST_FAILURE);
784 }
785 jsErrCode jerr = 0;
786 jsPubAck *pa = NULL;
787 natsStatus s = js_PublishMsg(&pa, ns->js, msg, NULL, &jerr);
788 if (pa) jsPubAck_Destroy(pa);
790 return s == NATS_OK ? jq_err(0) : jq_err(CWIST_FAILURE);
791}
792
795 natsMsg *dm = NULL;
796 if (natsMsg_Create(&dm, ns->dead_subject, NULL, natsMsg_GetData(msg),
798 const char *type = NULL;
799 if (natsMsgHeader_Get(msg, "Cwist-Type", &type) == NATS_OK && type)
800 natsMsgHeader_Set(dm, "Cwist-Type", type);
801 jsErrCode jerr = 0;
802 jsPubAck *pa = NULL;
803 if (js_PublishMsg(&pa, ns->js, dm, NULL, &jerr) == NATS_OK && pa) jsPubAck_Destroy(pa);
805 }
807}
808
816 jq_nats_t *ns = &q->u.nats;
817 uint64_t deadline = jq_now_ms() + timeout_ms;
818 for (;;) {
819 uint64_t now = jq_now_ms();
820 int64_t slice = (int64_t)(now >= deadline ? 1 : deadline - now);
824 memset(&list, 0, sizeof(list));
826 if (s != NATS_OK) {
827 if (s == NATS_TIMEOUT) {
828 if (jq_now_ms() >= deadline) return jq_err(CWIST_ERROR_TIMEOUT);
829 continue;
830 }
831 return jq_err(CWIST_FAILURE);
832 }
833 for (int i = 0; i < list.Count; i++) {
834 natsMsg *msg = list.Msgs[i];
835 list.Msgs[i] = NULL; /* keep the list from double-destroying it */
839 continue;
840 }
841 uint64_t delivered = meta->NumDelivered;
842 if (delivered > (uint64_t)q->max_retries) {
843 /* Max deliveries exhausted: park and terminate. */
847 continue;
848 }
849 cwist_job_t *j = cwist_alloc(sizeof(*j));
850 if (!j) {
854 return jq_err(CWIST_ERROR_NOMEM);
855 }
856 memset(j, 0, sizeof(*j));
857 j->q = q;
858 j->backend = msg; /* ownership moves to the job */
859 char id_buf[32];
860 snprintf(id_buf, sizeof(id_buf), "%llu", (unsigned long long)meta->Sequence.Stream);
861 j->id = cwist_strdup(id_buf);
863 j->payload_len = dlen > 0 ? (size_t)dlen : 0;
864 if (j->payload_len) {
865 j->payload = cwist_alloc(j->payload_len);
866 if (j->payload) memcpy(j->payload, natsMsg_GetData(msg), j->payload_len);
867 }
868 const char *type = NULL;
869 if (natsMsgHeader_Get(msg, "Cwist-Type", &type) == NATS_OK && type && *type)
870 j->type = cwist_strdup(type);
871 j->attempts = (uint32_t)delivered;
873 if (!j->id || (j->payload_len && !j->payload)) {
874 cwist_job_free(j);
876 return jq_err(CWIST_ERROR_NOMEM);
877 }
878 natsMsgList_Destroy(&list); /* frees the array, not the claimed msg */
879 *job = j;
880 return jq_err(0);
881 }
883 if (jq_now_ms() >= deadline) return jq_err(CWIST_ERROR_TIMEOUT);
884 }
885}
886
889 (void)q;
890 natsStatus s = natsMsg_Ack((natsMsg *)job->backend, NULL);
891 natsMsg_Destroy((natsMsg *)job->backend);
892 job->backend = NULL;
893 return s == NATS_OK ? jq_err(0) : jq_err(CWIST_FAILURE);
894}
895
898 natsStatus s;
899 if (q->retry_delay_ms)
900 s = natsMsg_NakWithDelay((natsMsg *)job->backend, (int64_t)q->retry_delay_ms, NULL);
901 else
902 s = natsMsg_Nak((natsMsg *)job->backend, NULL);
903 natsMsg_Destroy((natsMsg *)job->backend);
904 job->backend = NULL;
905 return s == NATS_OK ? jq_err(0) : jq_err(CWIST_FAILURE);
906}
907
910 jq_nats_t *ns = &q->u.nats;
911 jsErrCode jerr = 0;
913 if (js_GetStreamInfo(&si, ns->js, ns->dead_stream, NULL, &jerr) != NATS_OK)
914 return jq_err(CWIST_FAILURE);
915 *out_count = (uint64_t)si->State.Msgs;
917 return jq_err(0);
918}
919#else
920/* CWIST_NATS=0: the JetStream backend is compiled out; creation reports
921 * unsupported the same way an invalid wrapper does. */
928#endif /* CWIST_NATS */
929
930/* --- Dispatch ----------------------------------------------------------- */
931
934 if (!q) return;
935 if (q->backend == 0) {
936 cwist_free(q->u.redis.stream);
937 cwist_free(q->u.redis.attempts);
938 cwist_free(q->u.redis.dead);
939 cwist_free(q->u.redis.delay);
940 cwist_free(q->u.redis.consumer);
941 }
942#ifdef CWIST_NATS
943 else {
944 if (q->u.nats.sub) natsSubscription_Destroy(q->u.nats.sub);
945 if (q->u.nats.js) jsCtx_Destroy(q->u.nats.js);
946 cwist_free(q->u.nats.subject);
947 cwist_free(q->u.nats.dead_subject);
948 cwist_free(q->u.nats.dead_stream);
949 }
950#endif
951 cwist_free(q->name);
952 cwist_free(q->consumer);
953 cwist_free(q);
954}
955
960cwist_error_t cwist_job_queue_enqueue(cwist_job_queue_t *q, const void *payload, size_t payload_len,
961 const char *type) {
962 if (!q || (!payload && payload_len)) return jq_err(CWIST_ERROR_INVALID_PARAM);
963 if (!payload) payload = "";
964#ifdef CWIST_NATS
965 return q->backend == 0 ? jq_redis_enqueue(q, payload, payload_len, type)
966 : jq_nats_enqueue(q, payload, payload_len, type);
967#else
968 return jq_redis_enqueue(q, payload, payload_len, type);
969#endif
970}
971
977 if (!q || !job) return jq_err(CWIST_ERROR_INVALID_PARAM);
978 *job = NULL;
979#ifdef CWIST_NATS
980 return q->backend == 0 ? jq_redis_claim(q, timeout_ms, job) : jq_nats_claim(q, timeout_ms, job);
981#else
982 return jq_redis_claim(q, timeout_ms, job);
983#endif
984}
985
991 if (!q || !job || job->q != q) return jq_err(CWIST_ERROR_INVALID_PARAM);
992#ifdef CWIST_NATS
993 cwist_error_t err = q->backend == 0 ? jq_redis_ack(q, job) : jq_nats_ack(q, job);
994#else
995 cwist_error_t err = jq_redis_ack(q, job);
996#endif
997 cwist_job_free(job);
998 return err;
999}
1000
1006 if (!q || !job || job->q != q) return jq_err(CWIST_ERROR_INVALID_PARAM);
1007#ifdef CWIST_NATS
1008 cwist_error_t err = q->backend == 0 ? jq_redis_nack(q, job) : jq_nats_nack(q, job);
1009#else
1010 cwist_error_t err = jq_redis_nack(q, job);
1011#endif
1012 cwist_job_free(job);
1013 return err;
1014}
1015
1022 if (!q || !out_count) return jq_err(CWIST_ERROR_INVALID_PARAM);
1023 if (!q->dead_count) return jq_err(CWIST_FAILURE);
1024 return q->dead_count(q, out_count);
1025}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
Definition alloc.c:32
char * cwist_strndup(const char *src, size_t n)
Duplicate up to n bytes of a string (libttak-backed).
Definition alloc.c:41
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
NATS client interface.
natsConnection * cwist_nats_native(cwist_nats_t *nats)
Borrow the underlying cnats connection.
Definition cwist_nats.c:180
struct cwist_nats cwist_nats_t
Definition cwist_nats.h:16
struct __natsConnection natsConnection
Definition cwist_nats.h:18
Minimal in-tree Redis client (RESP2) with connection pooling.
struct cwist_redis cwist_redis_t
Single Redis connection handle.
Definition cwist_redis.h:19
cwist_error_t cwist_redis_command_argv_reply(cwist_redis_t *r, size_t argc, const void *const *argv, const size_t *argv_lens, cwist_redis_reply_t **reply)
void cwist_redis_reply_free(cwist_redis_reply_t *reply)
const char * cwist_job_id(const cwist_job_t *job)
Return the backend entry id of the job, or NULL for a NULL job.
cwist_job_queue_t * cwist_job_queue_create_redis(cwist_redis_t *conn, const cwist_job_queue_opts_t *opts)
Create a durable job queue backed by Redis streams.
void cwist_job_queue_destroy(cwist_job_queue_t *q)
Destroy a queue and all backend resources it owns; NULL is a no-op. In-flight jobs are not touched.
#define CWIST_JQ_MAX_POLL_SLICE_MS
const void * cwist_job_payload(const cwist_job_t *job, size_t *len)
Return the job payload pointer and store its length in len (0 on NULL job).
cwist_error_t cwist_job_queue_ack(cwist_job_queue_t *q, cwist_job_t *job)
Acknowledge a claimed job and free it.
cwist_error_t cwist_job_queue_enqueue(cwist_job_queue_t *q, const void *payload, size_t payload_len, const char *type)
Enqueue a job payload with an optional type.
#define CWIST_JQ_GROUP
void cwist_job_destroy(cwist_job_t *job)
Public alias of cwist_job_free(); releases a job returned by claim.
cwist_error_t cwist_job_queue_nack(cwist_job_queue_t *q, cwist_job_t *job)
Negatively acknowledge a claimed job (retry or dead-letter) and free it.
uint32_t cwist_job_attempts(const cwist_job_t *job)
Return the delivery attempt count of the job (0 for a NULL job).
const char * cwist_job_type(const cwist_job_t *job)
Return the job type string, or NULL if none / NULL job.
cwist_error_t cwist_job_queue_dead_count(cwist_job_queue_t *q, uint64_t *out_count)
Store the number of dead-lettered jobs in *out_count.
cwist_error_t cwist_job_queue_claim(cwist_job_queue_t *q, uint64_t timeout_ms, cwist_job_t **job)
Claim one job, waiting up to timeout_ms.
cwist_error_t(* jq_dead_count_fn)(cwist_job_queue_t *q, uint64_t *out_count)
cwist_job_queue_t * cwist_job_queue_create_nats(cwist_nats_t *nats, const cwist_job_queue_opts_t *opts)
Create a durable queue over an existing NATS connection.
#define CWIST_JQ_DEFAULT_MAX_RETRIES
#define CWIST_JQ_DEFAULT_VISIBILITY_MS
Durable (persistent) job queue backends over Redis and NATS.
struct cwist_job_queue cwist_job_queue_t
struct cwist_job cwist_job_t
@ CWIST_ERR_INT16
used when checking common errcodes in Unix/Linux
Definition cwist_err.h:16
cwist_error_t make_error(cwist_errtype_t type)
Creates an error object initialized with the specified type.
Definition error.c:14
size_t count
Definition otel.c:58
size_t cap
Definition otel.c:56
Integrated error structure used throughout the system.
Definition cwist_err.h:72
const char * consumer
uint64_t visibility_timeout_ms
struct cwist_redis_reply ** element
Definition cwist_redis.h:85
#define CWIST_ERROR_TIMEOUT
Definition cwist_err.h:155
#define CWIST_ERROR_INVALID_PARAM
Definition cwist_err.h:153
#define CWIST_FAILURE
Definition cwist_err.h:150
#define CWIST_ERROR_NOMEM
Definition cwist_err.h:152