25#define _POSIX_C_SOURCE 200809L
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
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"
56 "redis.call('ZADD', KEYS[4], ARGV[5], ARGV[4])\n"
61static const char *JQ_REAP_SCRIPT =
62 "local items = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], "
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"
78static const char *JQ_GROUP_SCRIPT =
79 "local ok, err = pcall(redis.call, 'XGROUP', 'CREATE', KEYS[1], ARGV[1], "
81 "if ok then return 1 end\n"
82 "if string.match(err, 'BUSYGROUP') then return 2 end\n"
111struct cwist_job_queue {
132 unsigned char *payload;
140 e.error.err_i16 =
code;
151static unsigned int jq_consumer_seq = 0;
154static char *jq_default_consumer(
void) {
156 snprintf(buf,
sizeof(buf),
"w%d-%u", (
int)
getpid(), ++jq_consumer_seq);
162 if (!opts || !opts->
name || !*opts->
name)
return false;
166 q->visibility_timeout_ms =
170 if (!q->name || !q->consumer)
return false;
201 if (len) *len =
job->payload_len;
217 return job ?
job->attempts : 0;
223static char *jq_key(
const char *name,
const char *suffix) {
226 if (key)
snprintf(key,
n,
"cwist:jq:%s:%s", name, suffix);
238 if (!cwist_error_is_ok(&
err) || !*
reply) {
248 const size_t *
argv_lens,
long long *out) {
251 if (!cwist_error_is_ok(&
err))
return err;
261static char *jq_blob(
const char *type,
uint32_t attempts,
const void *payload,
size_t payload_len,
264 size_t len = 8 +
type_len + payload_len;
266 if (!blob)
return NULL;
271 blob[4] = (
char)(attempts >> 24);
272 blob[5] = (
char)(attempts >> 16);
273 blob[6] = (
char)(attempts >> 8);
274 blob[7] = (
char)attempts;
276 if (payload_len)
memcpy(blob + 8 +
type_len, payload, payload_len);
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};
301 if (!fields || fields->
type !=
'*')
return NULL;
302 for (
size_t i = 0;
i + 1 < fields->
elements;
i += 2) {
328 job->payload_len = payload && payload->
str ? payload->
len : 0;
329 if (
job->payload_len) {
333 if (!
job->id || (
job->payload_len && !
job->payload)) {
343 jq_redis_t *
rs = &q->u.redis;
345 const void *
argv[] = {
"HINCRBY",
rs->attempts, id,
one};
359 return cwist_error_is_ok(&
err);
364 const char *type,
const void *payload,
size_t payload_len,
366 jq_redis_t *
rs = &q->u.redis;
368 char *blob = jq_blob(type, attempts, payload, payload_len, &blob_len);
370 const void *
argv[] = {
"RPUSH",
rs->dead, blob};
378 const void *
dargv[] = {
"HDEL",
rs->attempts,
id};
385 jq_redis_t *
rs = &q->u.redis;
388 const char *
zero[] = {
"0-0"};
389 const char *
count[] = {
"128"};
397 if (!cwist_error_is_ok(&
err) || !
reply ||
reply->type !=
'*' ||
reply->elements < 2) {
403 if (entries && entries->
type ==
'*') {
426 jq_redis_t *
rs = &q->u.redis;
429 const char gt[] =
">";
431 "BLOCK",
block,
"STREAMS",
rs->stream,
gt};
445 if (!cwist_error_is_ok(&
err) || !
reply ||
reply->type !=
'*' ||
reply->elements < 1) {
451 if (
top &&
top->type ==
'*' &&
top->elements == 2 &&
top->element[1] &&
452 top->element[1]->type ==
'*') {
460 if (cwist_error_is_ok(&
derr)) {
474 jq_redis_t *
rs = &q->u.redis;
477 const char three[] =
"3";
478 const void *
argv[] = {
"EVAL", JQ_REAP_SCRIPT,
three,
rs->delay,
rs->stream,
rs->attempts,
now};
496 uint64_t deadline = jq_now_ms() + timeout_ms;
499 jq_redis_reap_delay(q);
510 found = jq_redis_readgroup(q,
block);
521 jq_redis_t *
rs = &q->u.redis;
527 const void *
dargv[] = {
"HDEL",
rs->attempts,
job->id};
540 jq_redis_t *
rs = &q->u.redis;
548 char *blob = jq_blob(
job->type, next,
job->payload,
job->payload_len, &blob_len);
550 const char five[] =
"5";
576 jq_redis_t *
rs = &q->u.redis;
577 const void *
argv[] = {
"LLEN",
rs->dead};
593 if (!conn)
return NULL;
597 if (!jq_opts_init(q, opts))
goto fail;
599 q->dead_count = jq_redis_dead_count;
600 jq_redis_t *
rs = &q->u.redis;
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");
607 if (!
rs->stream || !
rs->attempts || !
rs->dead || !
rs->delay || !
rs->consumer)
goto fail;
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);
636 size_t len =
strlen(name);
638 if (!out)
return NULL;
639 for (
size_t i = 0;
i < len;
i++) {
641 out[
i] = (
c >=
'a' &&
c <=
'z') || (
c >=
'A' &&
c <=
'Z') || (
c >=
'0' &&
c <=
'9') ||
663 memset(&cfg, 0,
sizeof(cfg));
687 if (!jq_opts_init(q, opts))
goto fail;
694 if (!base || !consumer) {
703 if (!
ns->subject || !
ns->dead_subject || !
ns->dead_stream) {
709 snprintf(
ns->dead_subject,
n + 6,
"%s.dead", base);
718 if (cwist_error_is_ok(&
serr))
720 if (!cwist_error_is_ok(&
serr)) {
732 cc.Durable = consumer;
734 cc.AckWait = (
int64_t)q->visibility_timeout_ms * 1000000;
735 cc.MaxDeliver = (
int64_t)q->max_retries + 1;
737 cc.FilterSubject =
ns->subject;
798 const char *type =
NULL;
817 uint64_t deadline = jq_now_ms() + timeout_ms;
833 for (
int i = 0;
i <
list.Count;
i++) {
864 if (
j->payload_len) {
868 const char *type =
NULL;
873 if (!
j->id || (
j->payload_len && !
j->payload)) {
899 if (q->retry_delay_ms)
935 if (q->backend == 0) {
963 if (!payload) payload =
"";
965 return q->backend == 0 ? jq_redis_enqueue(q, payload, payload_len, type)
968 return jq_redis_enqueue(q, payload, payload_len, type);
980 return q->backend == 0 ? jq_redis_claim(q, timeout_ms,
job) :
jq_nats_claim(q, timeout_ms,
job);
982 return jq_redis_claim(q, timeout_ms,
job);
1012 cwist_job_free(
job);
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
char * cwist_strndup(const char *src, size_t n)
Duplicate up to n bytes of a string (libttak-backed).
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
natsConnection * cwist_nats_native(cwist_nats_t *nats)
Borrow the underlying cnats connection.
struct cwist_nats cwist_nats_t
struct __natsConnection natsConnection
Minimal in-tree Redis client (RESP2) with connection pooling.
struct cwist_redis cwist_redis_t
Single Redis connection handle.
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.
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
cwist_error_t make_error(cwist_errtype_t type)
Creates an error object initialized with the specified type.
Integrated error structure used throughout the system.
uint64_t visibility_timeout_ms
struct cwist_redis_reply ** element
#define CWIST_ERROR_TIMEOUT
#define CWIST_ERROR_INVALID_PARAM
#define CWIST_ERROR_NOMEM