threading infra

This commit is contained in:
2026-08-28 22:46:58 +02:00
parent 97edfc59fb
commit 9ea5d69cc1
7 changed files with 530 additions and 15 deletions
+47 -5
View File
@@ -2,16 +2,58 @@
#define HAMMY_JOB_H
#include <concord/discord.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <hammy/types.h>
// A single slash-command option. Flattened out of the interaction event.
// Both strings are owned by the job.
typedef struct hammy_arg_t {
char* name;
char* value;
} hammy_arg_t;
// A unit of deferred work handed from the gateway thread to a worker.
//
// OWNERSHIP: a job is created by the gateway thread and, on a successful
// hammy_pool_push(), ownership transfers to the pool. After that the creating
// thread MUST NOT touch it again. The worker that pops it owns it and destroys
// it. On a failed push the caller still owns it and must destroy it itself.
struct hammy_job_t {
char* token; // Interaction token
char* token; // Interaction token - owning
u64snowflake id; // Interaction ID
u64snowflake user; // Interaction User ID
char* command;
char** args; // Might be useful to parse into a struct someday? TODO
int64_t queued_at; // Staleness checks
u64snowflake appId; // Application ID, needed to edit the original response
u64snowflake user; // Invoking User ID, rate limiting, logging, etc.
char* command; // Command name - owning
hammy_arg_t* args; // Array of size nArgs. Owning, array and contents.
size_t nArgs;
int64_t queuedAt; // Staleness checks, in ms. From discord_timestamp().
};
// Deep-copies everything the job needs out of the interaction event, so the
// event may be freed by Concord the moment the handler returns.
// Returns NULL on allocation failure.
hammy_job_t* hammy_job_create(struct discord* client, const struct discord_interaction* event);
// Frees the job and everything it owns. NULLs the passing reference.
// Safe to call with NULL or with a pointer to NULL.
bool hammy_job_destroy(hammy_job_t** job);
// Looks up an option by name. Returns NULL if absent. Result is owned by the job.
const char* hammy_job_get_arg(const hammy_job_t* job, const char* name);
// Milliseconds elapsed since the job was created.
int64_t hammy_job_age_ms(const hammy_job_t* job, struct discord* client);
// Runs the job to completion and sends the reply. Called from a worker thread,
// so client MUST be that worker's own clone, never the gateway client.
// Does not destroy the job.
void hammy_job_run(hammy_job_t* job, struct discord* client);
// Edits the (already deferred) interaction response with a plain text body.
// Used by hammy_job_run() and by the pool's error paths.
void hammy_job_reply(const hammy_job_t* job, struct discord* client, const char* content);
#endif
+52 -5
View File
@@ -1,22 +1,69 @@
#ifndef HAMMY_POOL_H
#define HAMMY_POOL_H
#include <hammy/types.h>
#include <concord/discord.h>
#include <pthread.h>
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <hammy/types.h>
// TODO: Change this accordingly; also probably make it configurable without rebuilding at some point
#define HAMMY_POOL_DEFAULT_WORKERS 2
#define HAMMY_POOL_DEFAULT_CAPACITY 64
// Technically, discord allows 15 minutes... but hell no. 30 seconds.
#define HAMMY_JOB_MAX_AGE_MS 30000
typedef enum {
HAMMY_PUSH_OK = 0, // Queued. Ownership transferred to the pool.
HAMMY_PUSH_FULL = 1, // At capacity. Caller still owns the job.
HAMMY_PUSH_SHUTDOWN = 2 // Pool is closing. Caller still owns the job.
} hammy_push_result_t;
struct hammy_pool_t {
pthread_mutex_t lock;
pthread_cond_t notEmpty;
hammy_job_t** jobs; // ring buffer
size_t head;
size_t tail;
size_t count;
hammy_job_t** jobs; // Ring buffer of cap job pointers - owning while queued
size_t head; // Next slot to pop
size_t tail; // Next slot to push
size_t count; // Live entries - tracked so full != empty ambiguity doesn't exist
size_t cap;
size_t busy; // Workers currently processing jobs.
bool shutdown;
hammy_worker_t* workers;
size_t nWorkers;
};
// Creates the pool and starts n_workers threads, each with its own
// discord_clone() of client. Pass 0 for either size to take the defaults.
// Returns NULL on failure; no threads are left running in that case.
hammy_pool_t* hammy_pool_create(struct discord* client, size_t nWorkers, size_t queueCap);
// Enqueues a job. See hammy_push_result_t for who owns the job afterwards.
// Never blocks on anything but the (uncontended, short) queue mutex, so it is
// safe to call from the gateway thread.
hammy_push_result_t hammy_pool_push(hammy_pool_t* pool, hammy_job_t* job);
// Signals all workers to finish the queue and exit, then joins them.
// Idempotent. Queued jobs are still run, so an in-flight command still gets a
// reply; use hammy_pool_shutdown_now() if you would rather drop them.
void hammy_pool_shutdown(hammy_pool_t* pool);
// As above but discards anything still queued (each dropped job gets an
// apology reply if it can be sent quickly).
void hammy_pool_shutdown_now(hammy_pool_t* pool);
// Frees the pool. Runs hammy_pool_shutdown() first if it has not happened yet.
// NULLs the passing reference.
bool hammy_pool_destroy(hammy_pool_t** pool);
// Snapshot of queue depth and busy workers, for a /stats command or logging.
void hammy_pool_stats(hammy_pool_t* pool, size_t* outQueued, size_t* outBusy);
#endif
+12 -3
View File
@@ -7,9 +7,18 @@
struct hammy_worker_t {
pthread_t thread;
struct discord* clientCopy; // Clone of the client for concord threading safety
hammy_pool_t* pool;
int id;
struct discord* clientCopy; // Clone of the client for concord threading safety - owning
hammy_pool_t* pool; // Non-owning back-reference
int id; // Log logging mainly
bool started; // For joining
};
// Clones client, spawns the thread. Returns false on clone or spawn failure,
// leaving the worker safe to pass to hammy_worker_join().
bool hammy_worker_start(hammy_worker_t* worker, hammy_pool_t* pool, struct discord* client, int id);
// Joins the thread if it was started and cleans up the clone.
// The caller must have already set pool->shutdown and broadcast, or this hangs.
void hammy_worker_join(hammy_worker_t* worker);
#endif