curl-util: bring CurlGlue/CurlSlot in line with sd-bus and qmp-client

Refactor curl-util to use the same per-request, refcounted, cancellable
slot model as sd-bus, sd-varlink and qmp-client.

CurlGlue becomes opaque and refcounted, and dispatches per-slot
completion callbacks through CURLOPT_PRIVATE instead of a single
g->on_finished demux that every caller had to switch on. The new
curl_glue_perform_async(g, easy, cb, userdata, &slot) replaces
curl_glue_add + the on_finished/userdata wiring.

CurlSlot is the per-request handle: it owns the easy handle,
curl_slot_unref does curl_multi_remove_handle + curl_easy_cleanup
(which doubles as cancel since remove aborts in-flight transfers
without queuing CURLMSG_DONE), and floating slots (ret_slot=NULL) are
kept alive in the glue's slot set until the callback fires. Drop the
userdata parameter from curl_glue_make: CURLOPT_PRIVATE is now used
internally to route completions to the slot.

Migrate pull-job and the pull-{oci,raw,tar} drivers, and imdsd, to the
new shape. PullJob.curl becomes PullJob.slot; pull_job_curl_on_finished
becomes a per-slot callback. imdsd routes its token-vs-data branch off
slot identity rather than easy-handle pointer comparison. Both daemons
drop the global on_finished/userdata wiring on the glue. pull_job_finish
and context_fail{,_full} now return int (always 0) so the callbacks
stay in the `return finish(...);` style.

Add test-curl-util covering glue lifecycle, easy-handle defaults,
floating and non-floating perform paths, cancel-via-slot-unref (verified
by a sentinel request that drives the loop to completion), and three
concurrent requests on a single glue. Tests fetch local files via
file:// URLs so no network is needed; libcurl availability is probed
once via dlopen_curl in intro().
This commit is contained in:
Daan De Meyer
2026-05-08 16:52:57 +02:00
parent 0a5e09404d
commit 87cec65cae
11 changed files with 588 additions and 202 deletions
+159 -23
View File
@@ -12,6 +12,7 @@
#include "dlfcn-util.h"
#include "fd-util.h"
#include "hashmap.h"
#include "set.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
@@ -42,6 +43,77 @@ DLSYM_PROTOTYPE(curl_slist_free_all) = NULL;
DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(CURLM*, sym_curl_multi_cleanup, curl_multi_cleanupp, NULL);
struct CurlGlue {
unsigned n_ref;
sd_event *event;
CURLM *curl;
sd_event_source *timer;
Hashmap *ios;
sd_event_source *defer;
Set *slots; /* CurlSlot* — back-pointer set; floating slots are kept alive here */
};
struct CurlSlot {
unsigned n_ref;
CurlGlue *glue; /* NULL once disconnected (callback fired, cancelled, or glue died) */
CURL *easy; /* owned; cleared once the easy handle has been freed */
bool floating;
curl_finished_t callback;
void *userdata;
};
static void curl_slot_disconnect(CurlSlot *slot, bool unref) {
assert(slot);
/* Tear down the slot's connection to the glue: pull the easy handle out of the multi,
* curl_easy_cleanup() it, and remove the slot from the glue's lookup set. Floating
* slots are owned by that set, so on disconnect we drop the implicit ref (when
* unref=true; the recursive call from curl_slot_free passes false to avoid infinite
* recursion). Non-floating slots release the back-ref they held on the glue.
*
* Idempotent: once slot->glue is NULL, subsequent calls are no-ops. */
if (!slot->glue)
return;
CurlGlue *glue = slot->glue;
if (slot->easy) {
if (glue->curl)
(void) sym_curl_multi_remove_handle(glue->curl, slot->easy);
sym_curl_easy_cleanup(slot->easy);
slot->easy = NULL;
}
set_remove(glue->slots, slot);
slot->glue = NULL;
if (!slot->floating)
curl_glue_unref(glue);
else if (unref)
curl_slot_unref(slot);
}
static CurlSlot* curl_slot_free(CurlSlot *slot) {
if (!slot)
return NULL;
curl_slot_disconnect(slot, /* unref= */ false);
return mfree(slot);
}
DEFINE_TRIVIAL_REF_UNREF_FUNC(CurlSlot, curl_slot, curl_slot_free);
CURL* curl_slot_get_easy(CurlSlot *slot) {
assert(slot);
return slot->easy;
}
CurlGlue* curl_slot_get_glue(CurlSlot *slot) {
assert(slot);
return slot->glue;
}
static void curl_glue_check_finished(CurlGlue *g) {
int r;
@@ -60,8 +132,27 @@ static void curl_glue_check_finished(CurlGlue *g) {
if (!msg)
return;
if (msg->msg == CURLMSG_DONE && g->on_finished)
g->on_finished(g, msg->easy_handle, msg->data.result);
if (msg->msg == CURLMSG_DONE) {
CURL *easy = msg->easy_handle;
CURLcode code = msg->data.result;
CurlSlot *slot = NULL;
if (sym_curl_easy_getinfo(easy, CURLINFO_PRIVATE, (char**) &slot) == CURLE_OK && slot) {
/* Pin the slot across the callback: a floating slot's only
* reference is the one held via the glue's slots set, and
* disconnect drops it. */
curl_slot_ref(slot);
if (slot->callback) {
r = slot->callback(slot, easy, code, slot->userdata);
if (r < 0)
log_debug_errno(r, "Curl finished callback returned error, ignoring: %m");
}
curl_slot_disconnect(slot, /* unref= */ true);
curl_slot_unref(slot);
}
}
/* This is a queue, process another item soon, but do so in a later event loop iteration. */
(void) sd_event_source_set_enabled(g->defer, SD_EVENT_ONESHOT);
@@ -212,12 +303,22 @@ static int curl_glue_on_defer(sd_event_source *s, void *userdata) {
return 0;
}
CurlGlue *curl_glue_unref(CurlGlue *g) {
static CurlGlue* curl_glue_free(CurlGlue *g) {
sd_event_source *io;
CurlSlot *slot;
if (!g)
return NULL;
/* Drain any slots still hanging off us. By construction only floating slots can
* be here: connected non-floating slots hold a glue back-ref, so glue's last ref
* couldn't have dropped while one was attached. disconnect(unref=true) does the
* floating slot's free as part of its work. set_steal_first() pops up front so
* forward progress doesn't depend on disconnect's internal set_remove(). */
while ((slot = set_steal_first(g->slots)))
curl_slot_disconnect(slot, /* unref= */ true);
g->slots = set_free(g->slots);
if (g->curl)
sym_curl_multi_cleanup(g->curl);
@@ -232,6 +333,8 @@ CurlGlue *curl_glue_unref(CurlGlue *g) {
return mfree(g);
}
DEFINE_TRIVIAL_REF_UNREF_FUNC(CurlGlue, curl_glue, curl_glue_free);
int curl_glue_new(CurlGlue **glue, sd_event *event) {
_cleanup_(curl_glue_unrefp) CurlGlue *g = NULL;
_cleanup_(curl_multi_cleanupp) CURLM *c = NULL;
@@ -261,6 +364,7 @@ int curl_glue_new(CurlGlue **glue, sd_event *event) {
return -ENOMEM;
*g = (CurlGlue) {
.n_ref = 1,
.event = TAKE_PTR(e),
.curl = TAKE_PTR(c),
};
@@ -288,7 +392,7 @@ int curl_glue_new(CurlGlue **glue, sd_event *event) {
return 0;
}
int curl_glue_make(CURL **ret, const char *url, void *userdata) {
int curl_glue_make(CURL **ret, const char *url) {
_cleanup_(curl_easy_cleanupp) CURL *c = NULL;
const char *useragent;
int r;
@@ -310,9 +414,6 @@ int curl_glue_make(CURL **ret, const char *url, void *userdata) {
if (sym_curl_easy_setopt(c, CURLOPT_URL, url) != CURLE_OK)
return -EIO;
if (sym_curl_easy_setopt(c, CURLOPT_PRIVATE, userdata) != CURLE_OK)
return -EIO;
useragent = strjoina(program_invocation_short_name, "/" GIT_VERSION);
if (sym_curl_easy_setopt(c, CURLOPT_USERAGENT, useragent) != CURLE_OK)
return -EIO;
@@ -342,28 +443,63 @@ int curl_glue_make(CURL **ret, const char *url, void *userdata) {
return 0;
}
int curl_glue_add(CurlGlue *g, CURL *c) {
assert(g);
assert(c);
int curl_glue_perform_async(
CurlGlue *g,
CURL *easy,
curl_finished_t cb,
void *userdata,
CurlSlot **ret_slot) {
if (sym_curl_multi_add_handle(g->curl, c) != CURLM_OK)
int r;
assert(g);
assert(easy);
_cleanup_(curl_slot_unrefp) CurlSlot *slot = new(CurlSlot, 1);
if (!slot)
return -ENOMEM;
*slot = (CurlSlot) {
.n_ref = 1,
.glue = NULL, /* wired up below, after we've committed to the multi */
.easy = easy,
.floating = !ret_slot,
.callback = cb,
.userdata = userdata,
};
r = set_ensure_put(&g->slots, &trivial_hash_ops, slot);
if (r < 0)
return r;
assert(r > 0);
if (sym_curl_multi_add_handle(g->curl, easy) != CURLM_OK) {
set_remove(g->slots, slot);
return -EIO;
}
/* Stash the slot pointer on the easy handle so curl_glue_check_finished() can recover
* it on completion. Set this only after we've fully committed to the multi, so that
* error paths above don't leave a dangling pointer on the easy handle. */
if (sym_curl_easy_setopt(easy, CURLOPT_PRIVATE, slot) != CURLE_OK) {
sym_curl_multi_remove_handle(g->curl, easy);
set_remove(g->slots, slot);
return -EIO;
}
slot->glue = g;
if (!slot->floating)
curl_glue_ref(g);
/* Transfer the slot's single reference: to the caller for non-floating slots, or to
* the glue's slot set (implicitly, until disconnect drops it) for floating ones. */
if (ret_slot)
*ret_slot = slot;
TAKE_PTR(slot);
return 0;
}
void curl_glue_remove_and_free(CurlGlue *g, CURL *c) {
assert(g);
if (!c)
return;
if (g->curl)
sym_curl_multi_remove_handle(g->curl, c);
sym_curl_easy_cleanup(c);
}
struct curl_slist *curl_slist_new(const char *first, ...) {
struct curl_slist *l;
va_list ap;
+30 -15
View File
@@ -30,27 +30,42 @@ extern DLSYM_PROTOTYPE(curl_slist_free_all);
code == CURLE_OK; \
})
typedef struct CurlGlue CurlGlue;
typedef struct CurlGlue {
sd_event *event;
CURLM *curl;
sd_event_source *timer;
Hashmap *ios;
sd_event_source *defer;
void (*on_finished)(CurlGlue *g, CURL *curl, CURLcode code);
void *userdata;
} CurlGlue;
typedef int (*curl_finished_t)(CurlSlot *slot, CURL *curl, CURLcode code, void *userdata);
int curl_glue_new(CurlGlue **glue, sd_event *event);
CurlGlue* curl_glue_ref(CurlGlue *glue);
CurlGlue* curl_glue_unref(CurlGlue *glue);
DEFINE_TRIVIAL_CLEANUP_FUNC(CurlGlue*, curl_glue_unref);
int curl_glue_make(CURL **ret, const char *url, void *userdata);
int curl_glue_add(CurlGlue *g, CURL *c);
void curl_glue_remove_and_free(CurlGlue *g, CURL *c);
/* Build a CURL easy handle with sane defaults. The caller configures any
* additional options (headers, write callbacks, …) before handing it off to
* curl_glue_perform_async(). */
int curl_glue_make(CURL **ret, const char *url);
/* Hand a configured CURL easy handle off to the multi for execution. The slot
* takes ownership of the easy handle: once the slot is released (the callback
* has fired, the caller has dropped its last ref, or the glue is being freed),
* the handle is removed from the multi and freed.
*
* If ret_slot is NULL the slot is allocated as floating: the glue keeps it
* alive until the callback fires or the glue is torn down. Otherwise a
* reference is returned to the caller; releasing that reference cancels the
* call. */
int curl_glue_perform_async(
CurlGlue *g,
CURL *easy,
curl_finished_t cb,
void *userdata,
CurlSlot **ret_slot);
CURL* curl_slot_get_easy(CurlSlot *slot);
CurlGlue* curl_slot_get_glue(CurlSlot *slot);
CurlSlot* curl_slot_ref(CurlSlot *slot);
CurlSlot* curl_slot_unref(CurlSlot *slot);
DEFINE_TRIVIAL_CLEANUP_FUNC(CurlSlot*, curl_slot_unref);
struct curl_slist *curl_slist_new(const char *first, ...) _sentinel_;
int curl_header_strdup(const void *contents, size_t sz, const char *field, char **value);
+2
View File
@@ -57,6 +57,8 @@ typedef struct Condition Condition;
typedef struct ConfigSection ConfigSection;
typedef struct ConfigTableItem ConfigTableItem;
typedef struct CPUSet CPUSet;
typedef struct CurlGlue CurlGlue;
typedef struct CurlSlot CurlSlot;
typedef struct DissectedImage DissectedImage;
typedef struct DnsAnswer DnsAnswer;
typedef struct DnsPacket DnsPacket;