From 0167e36e749114a138eae0b05bb6a9bf804b170a Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 22 May 2026 06:36:52 +0000 Subject: [PATCH] sd-future: rework cancellation, add slot callbacks and future groups We replace the implicit single callback and wait-future indirection with the usual slot design so independent fibers and future groups can track the same future. We also opt to always have callbacks dispatch asynchronously, including when they are registered after the future has already resolved. We now require futures to be resolved before their final unref rather than cancelling them implicitly. The idea is to catch bugs rather than leak futures. A future should always be cancelled and awaited before it is cleaned up, otherwise resources might leak if the stack is not unwound properly. We make exceptions for futures that haven't started executing yet as there's no risk of leaking resources there since there's no stack to unwind. One issue with awaiting futures is that sd_fiber_await() returned both errors and the future's result via the function's return value, meaning it was impossible to distinguish future failures from interrupted waits (or waits that failed for whatever other reason). This is generally not an issue except in the cleanup functions, where we have to retry on interrupted waits (the best example is a parent fiber waiting for a future being cancelled itself, which should cancel the future it's waiting for and retry the wait). So we introduce fiber_wait() which distinguishes between both values via return value and output argument so the two can be distinguished from each other. Cleanup always consumes the caller's reference, including on invalid self or cross-loop waits. The existing assertion still forbids dropping the last reference to an unresolved future. The installed header documents this rule. We introduce a new RUNNING fiber state to distinguish executing fibers from non-executing fibers. We replace CANCELLED with queued resume results and preserve interruptions across ordinary wakeups. We add future groups with wait-all, wait-any and ignore-errors policies to wait for multiple futures at once and migrate various existing call sites over to them. Group size is a direct size_t query, treating NULL as an empty group. Document all four policy combinations, including that IGNORE_ERRORS suppresses fail-fast and parent cancellation without discarding the group's error result. We also add defer futures and migrate bus, varlink, QMP and fiber I/O callers to use them. Bus and QMP futures report -ENOPKG when no event loop is attached, rather than passing NULL to sd_future_new() and returning -EINVAL. Parent-await suppression is derived from the active wait's callback slot, so groups use sd_fiber_await() without a separate await API or additional state. Waits, group children and captured parents must use the same event loop. Groups created on another loop have no parent, and a group releases its parent reference when that fiber finishes. Resolve SD_EVENT_DEFAULT in sd_future_new() so futures retain the actual loop. Match group parents against that resolved loop as well; an unavailable default returns -ENOPKG without creating an event loop implicitly. Keep the public await entry point in fiber.c, sharing the slot-based wait with cancellation cleanup through future-internal.h. Keep event/future integration in event-future.c/h: priority inheritance uses the public fiber accessor, and sd_future_new_defer() lives alongside the other event-backed constructors. This avoids sd-event depending on sd-future private headers and removes the redundant private defer wrapper. Assert the non-failing slot-disabling and active-loop invariants instead of returning early without completing callback dispatch. Regression tests cover group error propagation and NULL-safe size queries, interrupted waits, partial add-many rollback, groups outliving their parent, and cancellation or timeout during nested event-loop waits, including event-loop and descriptor cleanup. They also cover cross-loop parent isolation, reference release on invalid cleanup calls, mixed WAIT_ANY outcomes, failed child cancellation and policy changes on resolved groups. Additional tests verify failed group-priority updates, slot priority during dispatch and exit, and cancellation, timeout and spurious wakeups during bus and QMP calls. Late replies must leave subsequent calls unaffected. Default-event tests cover missing-loop errors, reference lifetime and parent capture by constructors. Unattached bus and QMP clients are covered by missing-event-loop regressions. Constructor tests verify inherited and default priorities for I/O, defer and timer futures, plus defer argument validation and missing-default errors. Signed-off-by: Daan De Meyer --- src/libsystemd/forward.h | 3 +- src/libsystemd/meson.build | 2 + src/libsystemd/sd-bus/bus-future.c | 7 +- src/libsystemd/sd-bus/bus-future.h | 1 + src/libsystemd/sd-bus/bus-objects.c | 12 +- src/libsystemd/sd-bus/test-bus-fiber.c | 117 ++ src/libsystemd/sd-event/event-future.c | 182 ++- src/libsystemd/sd-event/event-future.h | 7 + src/libsystemd/sd-event/event-util.h | 2 + src/libsystemd/sd-event/sd-event.c | 2 +- src/libsystemd/sd-event/test-event-future.c | 223 +++ src/libsystemd/sd-future/fiber-io.c | 52 +- src/libsystemd/sd-future/fiber.c | 447 ++++-- src/libsystemd/sd-future/future-group.c | 322 ++++ src/libsystemd/sd-future/sd-future.c | 342 ++-- src/libsystemd/sd-future/test-fiber-io.c | 66 + src/libsystemd/sd-future/test-fiber.c | 1460 +++++++++++++++++- src/libsystemd/sd-future/test-future-group.c | 1156 ++++++++++++++ src/libsystemd/sd-varlink/sd-varlink.c | 4 +- src/shared/qmp-client.c | 7 +- src/systemd/sd-future.h | 69 +- src/test/test-qmp-client.c | 91 ++ 22 files changed, 4217 insertions(+), 357 deletions(-) create mode 100644 src/libsystemd/sd-future/future-group.c create mode 100644 src/libsystemd/sd-future/test-future-group.c diff --git a/src/libsystemd/forward.h b/src/libsystemd/forward.h index cb229f4621..307236fd36 100644 --- a/src/libsystemd/forward.h +++ b/src/libsystemd/forward.h @@ -125,7 +125,8 @@ typedef struct sd_resolve_query sd_resolve_query; typedef struct sd_hwdb sd_hwdb; typedef struct sd_future sd_future; +typedef struct sd_future_slot sd_future_slot; -typedef int (*sd_future_func_t)(sd_future *f); +typedef int (*sd_future_func_t)(sd_future *f, void *userdata); typedef int (*sd_fiber_func_t)(void *userdata); typedef _sd_destroy_t sd_fiber_destroy_t; diff --git a/src/libsystemd/meson.build b/src/libsystemd/meson.build index cd3821bb8d..d1f3a4763e 100644 --- a/src/libsystemd/meson.build +++ b/src/libsystemd/meson.build @@ -73,6 +73,7 @@ sd_device_sources = files( sd_future_sources = files( 'sd-future/fiber-io.c', 'sd-future/fiber.c', + 'sd-future/future-group.c', 'sd-future/sd-future.c', ) @@ -189,6 +190,7 @@ simple_tests += files( 'sd-future/test-fiber.c', 'sd-future/test-fiber-io.c', 'sd-future/test-fiber-ops.c', + 'sd-future/test-future-group.c', 'sd-hwdb/test-sd-hwdb.c', 'sd-id128/test-id128.c', 'sd-journal/test-catalog.c', diff --git a/src/libsystemd/sd-bus/bus-future.c b/src/libsystemd/sd-bus/bus-future.c index d7037b3f15..9e72ba2766 100644 --- a/src/libsystemd/sd-bus/bus-future.c +++ b/src/libsystemd/sd-bus/bus-future.c @@ -63,9 +63,10 @@ int bus_call_future(sd_bus *bus, sd_bus_message *m, uint64_t usec, sd_future **r assert(bus); assert(m); assert(ret); + assert_return(bus->event, -ENOPKG); - _cleanup_(sd_future_unrefp) sd_future *f = NULL; - r = sd_future_new(&bus_future_ops, &f); + _cleanup_(sd_future_cancel_unrefp) sd_future *f = NULL; + r = sd_future_new(sd_bus_get_event(bus), &bus_future_ops, &f); if (r < 0) return r; @@ -122,7 +123,7 @@ int bus_call_suspend( if (r < 0) return sd_bus_error_set_errno(reterr_error, r); - r = sd_fiber_suspend(); + r = sd_fiber_await(f); /* If the future isn't resolved, the suspend was interrupted before a reply arrived (fiber * cancelled, fiber-wide SD_FIBER_TIMEOUT scope expired, …). There's no reply to extract, diff --git a/src/libsystemd/sd-bus/bus-future.h b/src/libsystemd/sd-bus/bus-future.h index e96a43e825..f85a365bcb 100644 --- a/src/libsystemd/sd-bus/bus-future.h +++ b/src/libsystemd/sd-bus/bus-future.h @@ -3,6 +3,7 @@ #include "forward.h" +/* The bus must have an attached event loop; otherwise return -ENOPKG. */ int bus_call_future(sd_bus *bus, sd_bus_message *m, uint64_t usec, sd_future **ret); int future_get_bus_reply(sd_future *f, sd_bus_error *reterr_error, sd_bus_message **ret_reply); diff --git a/src/libsystemd/sd-bus/bus-objects.c b/src/libsystemd/sd-bus/bus-objects.c index aa371cceca..164e1dc059 100644 --- a/src/libsystemd/sd-bus/bus-objects.c +++ b/src/libsystemd/sd-bus/bus-objects.c @@ -373,8 +373,8 @@ DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( trivial_compare_func, bus_fiber_future_unref); -static int bus_fiber_resolved(sd_future *f) { - sd_bus *bus = ASSERT_PTR(sd_future_get_userdata(f)); +static int bus_fiber_resolved(sd_future *f, void *userdata) { + sd_bus *bus = ASSERT_PTR(userdata); assert_se(set_remove(bus->fiber_futures, f) == f); sd_future_unref(f); @@ -488,7 +488,7 @@ static int method_callbacks_run( .userdata = u, }; - _cleanup_(sd_future_unrefp) sd_future *f = NULL; + _cleanup_(sd_future_cancel_unrefp) sd_future *f = NULL; r = sd_fiber_new(bus->event, c->member, bus_fiber_entry, d, bus_fiber_data_destroy, &f); if (r < 0) return bus_maybe_reply_error(m, r, NULL); @@ -502,8 +502,10 @@ static int method_callbacks_run( return bus_maybe_reply_error(m, r, NULL); assert(r > 0); - /* Track the future on the bus so shutdown can cancel it and wait for it. */ - r = sd_future_set_callback(f, bus_fiber_resolved, bus); + /* Track the future on the bus so shutdown can cancel it and wait for it. + * Floating slot — tied to f's lifetime; bus_fiber_resolved is what drops + * the set's ref, which is the last ref and frees f (and the slot). */ + r = sd_future_add_callback(f, /* ret_slot= */ NULL, bus_fiber_resolved, bus); if (r < 0) { /* TAKE_PTR(f) hasn't run yet, so our cleanup attribute still owns the * ref; set_remove() returns the raw pointer without firing the hash_ops diff --git a/src/libsystemd/sd-bus/test-bus-fiber.c b/src/libsystemd/sd-bus/test-bus-fiber.c index 2cadf1dc17..179f8943da 100644 --- a/src/libsystemd/sd-bus/test-bus-fiber.c +++ b/src/libsystemd/sd-bus/test-bus-fiber.c @@ -6,6 +6,7 @@ #include "sd-event.h" #include "sd-future.h" +#include "bus-future.h" #include "bus-internal.h" #include "tests.h" #include "time-util.h" @@ -18,6 +19,7 @@ typedef struct Context { int in_flight; int max_in_flight; sd_future *waiter; + sd_bus_message *pending_call; } Context; static int method_concurrent(sd_bus_message *m, void *userdata, sd_bus_error *reterr_error) { @@ -67,6 +69,25 @@ static int method_fail_error(sd_bus_message *m, void *userdata, sd_bus_error *re return sd_bus_error_set(reterr_error, SD_BUS_ERROR_INVALID_ARGS, "bad arguments from fiber"); } +static int method_hold(sd_bus_message *m, void *userdata, sd_bus_error *reterr_error) { + Context *c = ASSERT_PTR(userdata); + + ASSERT_NULL(c->pending_call); + c->pending_call = sd_bus_message_ref(m); + if (c->waiter) + ASSERT_OK(sd_fiber_resume(TAKE_PTR(c->waiter), 0)); + return 1; +} + +static int method_release(sd_bus_message *m, void *userdata, sd_bus_error *reterr_error) { + Context *c = ASSERT_PTR(userdata); + + ASSERT_NOT_NULL(c->pending_call); + ASSERT_OK(sd_bus_reply_method_return(c->pending_call, /* types= */ NULL)); + c->pending_call = sd_bus_message_unref(c->pending_call); + return sd_bus_reply_method_return(m, /* types= */ NULL); +} + static const sd_bus_vtable vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_METHOD("Concurrent", NULL, NULL, method_concurrent, @@ -75,6 +96,8 @@ static const sd_bus_vtable vtable[] = { SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_METHOD_FIBER), SD_BUS_METHOD("FailError", NULL, NULL, method_fail_error, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_METHOD_FIBER), + SD_BUS_METHOD("Hold", NULL, NULL, method_hold, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("Release", NULL, NULL, method_release, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END, }; @@ -168,6 +191,17 @@ static int errors_fiber(void *userdata) { ASSERT_OK(attach_pair(s, &server, &client)); + /* A detached bus must report the missing loop at the future API boundary. Reattaching + * it restores the regular suspending call path used below. */ + _cleanup_(sd_bus_message_unrefp) sd_bus_message *call = NULL; + _cleanup_(sd_future_cancel_unrefp) sd_future *future = NULL; + ASSERT_OK(sd_bus_message_new_method_call(client, &call, /* destination= */ NULL, + "/test", "test.Fiber", "FailErrno")); + ASSERT_OK(sd_bus_detach_event(client)); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(bus_call_future(client, call, USEC_INFINITY, &future)), ENOPKG); + ASSERT_NULL(future); + ASSERT_OK(sd_bus_attach_event(client, sd_fiber_get_event(), 0)); + /* A fiber handler that returns a negative errno gets turned into a matching sd_bus error * reply (bus_maybe_reply_error → sd_bus_reply_method_errno). */ _cleanup_(sd_bus_error_free) sd_bus_error e1 = SD_BUS_ERROR_NULL; @@ -204,4 +238,87 @@ TEST(fiber_method_errors) { ASSERT_OK(sd_future_result(f)); } +typedef struct InterruptedCall { + sd_bus *client; + int error; +} InterruptedCall; + +static int interrupted_call_fiber(void *userdata) { + InterruptedCall *c = ASSERT_PTR(userdata); + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + + { + SD_FIBER_TIMEOUT(c->error == ETIME ? 0 : USEC_INFINITY); + ASSERT_ERROR(sd_bus_call_method(c->client, /* destination= */ NULL, + "/test", "test.Fiber", "Hold", &error, &reply, + /* types= */ NULL), c->error); + } + /* D-Bus maps both ETIME and ETIMEDOUT to its Timeout error name; the call's return value + * above still preserves the original interruption errno. */ + ASSERT_EQ(sd_bus_error_get_errno(&error), c->error == ETIME ? ETIMEDOUT : c->error); + if (c->error == ETIME) + ASSERT_TRUE(sd_bus_error_has_name(&error, SD_BUS_ERROR_TIMEOUT)); + ASSERT_NULL(reply); + sd_bus_error_free(&error); + + /* Release sends the abandoned call's reply before its own. The old reply must neither + * access a freed future nor wake this new wait, and no interruption may leak into it. */ + ASSERT_OK_POSITIVE(sd_bus_call_method(c->client, /* destination= */ NULL, + "/test", "test.Fiber", "Release", &error, &reply, + /* types= */ NULL)); + ASSERT_NOT_NULL(reply); + ASSERT_FALSE(sd_bus_error_is_set(&error)); + ASSERT_OK_ZERO(sd_fiber_yield()); + return 0; +} + +static int interrupted_calls_fiber(void *userdata) { + Setup *s = ASSERT_PTR(userdata); + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *server = NULL, *client = NULL; + int error; + + ASSERT_OK(attach_pair(s, &server, &client)); + + FOREACH_ARGUMENT(error, ECANCELED, ETIME, EBUSY) { + _cleanup_(sd_future_cancel_wait_unrefp) sd_future *caller = NULL; + InterruptedCall c = { .client = client, .error = error }; + + s->c->waiter = error == ETIME ? NULL : sd_fiber_get_current(); + ASSERT_OK(sd_fiber_new(sd_fiber_get_event(), "interrupted-call", interrupted_call_fiber, + &c, /* destroy= */ NULL, &caller)); + + if (error != ETIME) { + /* Interrupt only once the server has the call in hand and withheld its reply. */ + ASSERT_OK_ZERO(sd_fiber_suspend()); + ASSERT_NOT_NULL(s->c->pending_call); + if (error == ECANCELED) + ASSERT_OK(sd_future_cancel(caller)); + else + ASSERT_OK(sd_fiber_resume(caller, 42)); + } + + ASSERT_OK_ZERO(sd_fiber_await(caller)); + ASSERT_NULL(s->c->pending_call); + ASSERT_NULL(s->c->waiter); + } + + return 0; +} + +TEST(fiber_method_interrupted) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + Context c = {}; + Setup s = { .c = &c }; + + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, s.fds)); + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_fiber_new(e, "interrupted-calls", interrupted_calls_fiber, &s, + /* destroy= */ NULL, &f)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(f)); +} + DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/libsystemd/sd-event/event-future.c b/src/libsystemd/sd-event/event-future.c index 4e0a0a87fc..24ba19d678 100644 --- a/src/libsystemd/sd-event/event-future.c +++ b/src/libsystemd/sd-event/event-future.c @@ -9,6 +9,22 @@ #include "event-util.h" #include "fd-util.h" +int event_source_inherit_fiber_priority(sd_event_source *s) { + int64_t priority; + int r; + + assert(s); + + if (!sd_fiber_is_running()) + return 0; + + r = sd_fiber_get_priority(&priority); + if (r < 0) + return r; + + return sd_event_source_set_priority(s, priority); +} + typedef struct IoFuture { sd_event_source *source; } IoFuture; @@ -84,8 +100,8 @@ int future_new_io(sd_event *e, int fd, uint32_t events, sd_future **ret) { if (IN_SET(sd_event_get_state(e), SD_EVENT_EXITING, SD_EVENT_FINISHED)) return -ECANCELED; - _cleanup_(sd_future_unrefp) sd_future *f = NULL; - r = sd_future_new(&io_future_ops, &f); + _cleanup_(sd_future_cancel_unrefp) sd_future *f = NULL; + r = sd_future_new(e, &io_future_ops, &f); if (r < 0) return r; @@ -110,17 +126,101 @@ int future_new_io(sd_event *e, int fd, uint32_t events, sd_future **ret) { if (r < 0) return r; - if (sd_fiber_is_running()) { - int64_t priority; + r = event_source_inherit_fiber_priority(iof->source); + if (r < 0) + return r; - r = sd_fiber_get_priority(&priority); - if (r < 0) - return r; + *ret = TAKE_PTR(f); + return 0; +} - r = sd_event_source_set_priority(iof->source, priority); - if (r < 0) - return r; - } +int future_group_add_io(sd_future *group, int fd, uint32_t events) { + _cleanup_(sd_future_cancel_unrefp) sd_future *io = NULL; + int r; + + assert(group); + + r = future_new_io(sd_future_get_event(group), fd, events, &io); + if (r < 0) + return r; + + r = sd_future_group_add(group, io); + if (r < 0) + return r; + + /* The group owns a reference now: release ours without cancelling the child. */ + io = sd_future_unref(io); + return 0; +} + +typedef struct DeferFuture { + sd_event_source *source; + int result; +} DeferFuture; + +static void* defer_future_alloc(void) { + return new0(DeferFuture, 1); +} + +static void defer_future_free(sd_future *f) { + DeferFuture *df = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); + + sd_event_source_unref(df->source); + free(df); +} + +static int defer_future_cancel(sd_future *f) { + DeferFuture *df = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); + int r; + + r = sd_event_source_set_enabled(df->source, SD_EVENT_OFF); + RET_GATHER(r, sd_future_resolve(f, -ECANCELED)); + return r; +} + +static int defer_future_set_priority(sd_future *f, int64_t priority) { + DeferFuture *df = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); + return sd_event_source_set_priority(df->source, priority); +} + +static const sd_future_ops defer_future_ops = { + .size = sizeof(sd_future_ops), + .alloc = defer_future_alloc, + .free = defer_future_free, + .cancel = defer_future_cancel, + .set_priority = defer_future_set_priority, +}; + +static int defer_handler(sd_event_source *s, void *userdata) { + sd_future *f = ASSERT_PTR(userdata); + DeferFuture *df = ASSERT_PTR(sd_future_get_private(f)); + return sd_future_resolve(f, df->result); +} + +int sd_future_new_defer(sd_event *e, int result, sd_future **ret) { + int r; + + assert_return(e, -EINVAL); + assert_return(ret, -EINVAL); + + if (IN_SET(sd_event_get_state(e), SD_EVENT_EXITING, SD_EVENT_FINISHED)) + return -ECANCELED; + + _cleanup_(sd_future_cancel_unrefp) sd_future *f = NULL; + r = sd_future_new(e, &defer_future_ops, &f); + if (r < 0) + return r; + + DeferFuture *df = sd_future_get_private(f); + df->result = result; + + r = sd_event_add_defer(e, &df->source, defer_handler, f); + if (r < 0) + return r; + + r = event_source_inherit_fiber_priority(df->source); + if (r < 0) + return r; *ret = TAKE_PTR(f); return 0; @@ -200,8 +300,8 @@ static int future_new_time_internal( if (IN_SET(sd_event_get_state(e), SD_EVENT_EXITING, SD_EVENT_FINISHED)) return -ECANCELED; - _cleanup_(sd_future_unrefp) sd_future *f = NULL; - r = sd_future_new(&time_future_ops, &f); + _cleanup_(sd_future_cancel_unrefp) sd_future *f = NULL; + r = sd_future_new(e, &time_future_ops, &f); if (r < 0) return r; @@ -212,17 +312,9 @@ static int future_new_time_internal( if (r < 0) return r; - if (sd_fiber_is_running()) { - int64_t priority; - - r = sd_fiber_get_priority(&priority); - if (r < 0) - return r; - - r = sd_event_source_set_priority(tf->source, priority); - if (r < 0) - return r; - } + r = event_source_inherit_fiber_priority(tf->source); + if (r < 0) + return r; *ret = TAKE_PTR(f); return 0; @@ -236,6 +328,25 @@ int future_new_time_relative(sd_event *e, clockid_t clock, uint64_t usec, uint64 return future_new_time_internal(sd_event_add_time_relative, e, clock, usec, accuracy, result, ret); } +int future_group_add_time_relative(sd_future *group, clockid_t clock, uint64_t usec, uint64_t accuracy, int result) { + _cleanup_(sd_future_cancel_unrefp) sd_future *timer = NULL; + int r; + + assert(group); + + r = future_new_time_relative(sd_future_get_event(group), clock, usec, accuracy, result, &timer); + if (r < 0) + return r; + + r = sd_future_group_add(group, timer); + if (r < 0) + return r; + + /* The group owns a reference now: release ours without cancelling the child. */ + timer = sd_future_unref(timer); + return 0; +} + int event_run_suspend(sd_event *e, uint64_t timeout) { sd_event *outer = sd_fiber_get_event(); int r; @@ -271,25 +382,32 @@ int event_run_suspend(sd_event *e, uint64_t timeout) { if (fd < 0) return fd; - _cleanup_(sd_future_cancel_wait_unrefp) sd_future *io = NULL; - r = future_new_io(outer, fd, EPOLLIN, &io); + /* Wait for the inner-loop fd to become readable OR (optionally) the timeout to fire. */ + _cleanup_(sd_future_cancel_wait_unrefp) sd_future *group = NULL; + r = sd_future_group_new(outer, &group); + if (r < 0) + return r; + + r = sd_future_group_set_policy(group, SD_FUTURE_GROUP_WAIT_ANY); + if (r < 0) + return r; + + r = future_group_add_io(group, fd, EPOLLIN); if (r < 0) return r; - _cleanup_(sd_future_cancel_wait_unrefp) sd_future *timer = NULL; if (timeout != USEC_INFINITY) { - r = future_new_time_relative( - outer, + r = future_group_add_time_relative( + group, CLOCK_MONOTONIC, timeout, /* accuracy= */ 1, - /* result= */ 0, - &timer); + /* result= */ 0); if (r < 0) return r; } - r = sd_fiber_suspend(); + r = sd_fiber_await(group); if (r < 0) return r; diff --git a/src/libsystemd/sd-event/event-future.h b/src/libsystemd/sd-event/event-future.h index 9ae02cf4f6..9a9ef12106 100644 --- a/src/libsystemd/sd-event/event-future.h +++ b/src/libsystemd/sd-event/event-future.h @@ -3,8 +3,15 @@ #include "forward.h" +/* Event/future integration, implemented using only the public sd-future API. */ + int future_new_io(sd_event *e, int fd, uint32_t events, sd_future **ret); int future_new_time(sd_event *e, clockid_t clock, uint64_t usec, uint64_t accuracy, int result, sd_future **ret); int future_new_time_relative(sd_event *e, clockid_t clock, uint64_t usec, uint64_t accuracy, int result, sd_future **ret); +int future_group_add_io(sd_future *group, int fd, uint32_t events); +int future_group_add_time_relative(sd_future *group, clockid_t clock, uint64_t usec, uint64_t accuracy, int result); + +int event_source_inherit_fiber_priority(sd_event_source *s); + int event_run_suspend(sd_event *e, uint64_t timeout); diff --git a/src/libsystemd/sd-event/event-util.h b/src/libsystemd/sd-event/event-util.h index 06fff49c96..064ca2cce9 100644 --- a/src/libsystemd/sd-event/event-util.h +++ b/src/libsystemd/sd-event/event-util.h @@ -10,6 +10,8 @@ extern const struct hash_ops event_source_hash_ops; +sd_event* event_resolve(sd_event *e); + int event_reset_time( sd_event *e, sd_event_source **s, diff --git a/src/libsystemd/sd-event/sd-event.c b/src/libsystemd/sd-event/sd-event.c index dd293a89fd..23e6216b97 100644 --- a/src/libsystemd/sd-event/sd-event.c +++ b/src/libsystemd/sd-event/sd-event.c @@ -192,7 +192,7 @@ static thread_local sd_event *default_event = NULL; static void source_disconnect(sd_event_source *s); static void event_gc_inode_data(sd_event *e, InodeData *d); -static sd_event* event_resolve(sd_event *e) { +sd_event* event_resolve(sd_event *e) { return e == SD_EVENT_DEFAULT ? default_event : e; } diff --git a/src/libsystemd/sd-event/test-event-future.c b/src/libsystemd/sd-event/test-event-future.c index 754daf0df2..941dc10e03 100644 --- a/src/libsystemd/sd-event/test-event-future.c +++ b/src/libsystemd/sd-event/test-event-future.c @@ -6,10 +6,170 @@ #include "sd-event.h" #include "sd-future.h" +#include "event-future.h" #include "fd-util.h" #include "tests.h" #include "time-util.h" +typedef enum EventFutureType { + EVENT_FUTURE_IO, + EVENT_FUTURE_DEFER, + EVENT_FUTURE_TIME, + EVENT_FUTURE_TIME_RELATIVE, + _EVENT_FUTURE_TYPE_MAX, + _EVENT_FUTURE_TYPE_INVALID = -EINVAL, +} EventFutureType; + +static int new_event_future(EventFutureType type, sd_event *e, int fd, sd_future **ret) { + switch (type) { + case EVENT_FUTURE_IO: + return future_new_io(e, fd, EPOLLIN, ret); + case EVENT_FUTURE_DEFER: + return sd_future_new_defer(e, 0, ret); + case EVENT_FUTURE_TIME: + return future_new_time(e, CLOCK_MONOTONIC, 0, 1, 0, ret); + case EVENT_FUTURE_TIME_RELATIVE: + return future_new_time_relative(e, CLOCK_MONOTONIC, 0, 1, 0, ret); + default: + assert_not_reached(); + } +} + +typedef struct EventFuturePriorityData { + EventFutureType type; + int fd; + sd_future **ret; +} EventFuturePriorityData; + +static int event_future_priority_fiber(void *userdata) { + EventFuturePriorityData *d = ASSERT_PTR(userdata); + + return new_event_future(d->type, sd_fiber_get_event(), d->fd, d->ret); +} + +TEST(event_future_priority) { + int64_t priority, probe_priority; + + for (EventFutureType type = 0; type < _EVENT_FUTURE_TYPE_MAX; type++) + FOREACH_ARGUMENT(priority, -10, 0, 10) + FOREACH_ARGUMENT(probe_priority, -5, 5) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_close_pair_ int fds[2] = EBADF_PAIR; + _cleanup_(sd_future_cancel_unrefp) sd_future *f = NULL, *probe = NULL; + + ASSERT_OK(sd_event_new(&e)); + if (type == EVENT_FUTURE_IO) { + ASSERT_OK_ERRNO(pipe2(fds, O_CLOEXEC | O_NONBLOCK)); + ASSERT_OK_EQ_ERRNO(write(fds[1], "X", 1), 1); + } + + if (priority != 0) { + _cleanup_(sd_future_unrefp) sd_future *registrar = NULL; + EventFuturePriorityData d = { .type = type, .fd = fds[0], .ret = &f }; + + ASSERT_OK(sd_fiber_new(e, "new-event-future", event_future_priority_fiber, + &d, /* destroy= */ NULL, ®istrar)); + ASSERT_OK(sd_future_set_priority(registrar, priority)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_OK_ZERO(sd_future_result(registrar)); + } else + /* Construction outside a fiber must keep the normal priority. */ + ASSERT_OK(new_event_future(type, e, fds[0], &f)); + + ASSERT_EQ(sd_future_state(f), SD_FUTURE_PENDING); + ASSERT_OK(new_event_future(type, e, fds[0], &probe)); + ASSERT_OK(sd_future_set_priority(probe, probe_priority)); + + /* Both sources are ready. Inspect resolution directly, without callback + * slots whose own priority inheritance could hide a constructor bug. */ + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(f), + priority < probe_priority ? SD_FUTURE_RESOLVED : SD_FUTURE_PENDING); + ASSERT_EQ(sd_future_state(probe), + priority < probe_priority ? SD_FUTURE_PENDING : SD_FUTURE_RESOLVED); + } +} + +TEST(event_source_inherit_fiber_priority_without_fiber) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL; + int64_t priority; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_add_defer(e, &s, /* callback= */ NULL, /* userdata= */ NULL)); + ASSERT_OK(sd_event_source_set_priority(s, 42)); + ASSERT_OK(event_source_inherit_fiber_priority(s)); + ASSERT_OK(sd_event_source_get_priority(s, &priority)); + ASSERT_EQ(priority, 42); +} + +TEST(future_new_defer_invalid) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_cancel_unrefp) sd_future *f = NULL; + + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_new_defer(/* e= */ NULL, 0, &f)), EINVAL); + ASSERT_OK_ZERO(sd_event_default(/* ret= */ NULL)); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_new_defer(SD_EVENT_DEFAULT, 0, &f)), ENOPKG); + ASSERT_OK_ZERO(sd_event_default(/* ret= */ NULL)); + + ASSERT_OK(sd_event_new(&e)); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_new_defer(e, 0, /* ret= */ NULL)), EINVAL); + ASSERT_NULL(f); +} + +TEST(future_group_add_event) { + bool timer; + + FOREACH_ARGUMENT(timer, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *group = NULL; + _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_group_set_policy(group, SD_FUTURE_GROUP_WAIT_ANY)); + + if (timer) + ASSERT_OK(future_group_add_time_relative(group, CLOCK_MONOTONIC, 0, 1, 42)); + else { + ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK)); + ASSERT_OK(future_group_add_io(group, pipefd[0], EPOLLIN)); + ASSERT_OK_ZERO(sd_event_run(e, 0)); + ASSERT_OK_EQ_ERRNO(write(pipefd[1], "X", 1), 1); + } + + ASSERT_EQ(sd_future_group_size(group), 1U); + + /* The helper must leave the child alive and uncancelled, with the group owning its + * only reference, until its event fires. */ + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(sd_future_result(group), timer ? 42 : (int) EPOLLIN); + } +} + +TEST(future_group_add_event_failure) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *group = NULL; + _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR; + char c; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_cancel(group)); + ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK)); + ASSERT_OK_EQ_ERRNO(write(pipefd[1], "X", 1), 1); + + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(future_group_add_io(group, pipefd[0], EPOLLIN)), ESTALE); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(future_group_add_time_relative(group, CLOCK_MONOTONIC, 0, 1, 42)), ESTALE); + + /* Rejected children must leave no enabled sources behind, and releasing the IO child's + * duplicate fd must not close the caller's fd. */ + ASSERT_OK_ZERO(sd_event_run(e, 0)); + ASSERT_OK_EQ_ERRNO(read(pipefd[0], &c, 1), 1); + ASSERT_EQ(c, 'X'); +} + static int timer_callback(sd_event_source *s, uint64_t usec, void *userdata) { int *count = ASSERT_PTR(userdata); int r; @@ -355,4 +515,67 @@ TEST(sd_event_run_timer) { ASSERT_OK_ZERO(sd_future_result(f)); } +typedef struct EventRunInterruptedState { + sd_event *inner; + bool timeout; + bool finite; +} EventRunInterruptedState; + +static int event_run_interrupted_fiber(void *userdata) { + EventRunInterruptedState *s = ASSERT_PTR(userdata); + + SD_FIBER_TIMEOUT(s->timeout ? 5 * USEC_PER_MSEC : USEC_INFINITY); + return sd_event_run(s->inner, s->finite ? USEC_PER_MINUTE : USEC_INFINITY); +} + +TEST(sd_event_run_interrupted) { + bool timeout, finite; + + FOREACH_ARGUMENT(timeout, false, true) + FOREACH_ARGUMENT(finite, false, true) { + _cleanup_(sd_event_unrefp) sd_event *outer = NULL, *inner = NULL; + _cleanup_(sd_event_source_unrefp) sd_event_source *source = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR; + int count = 0; + + ASSERT_OK(sd_event_new(&outer)); + ASSERT_OK(sd_event_new(&inner)); + ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK)); + ASSERT_OK(sd_event_add_io(inner, &source, pipefd[0], EPOLLIN, io_callback, &count)); + + EventRunInterruptedState s = { .inner = inner, .timeout = timeout, .finite = finite }; + ASSERT_OK(sd_fiber_new(outer, "event-interrupted", event_run_interrupted_fiber, &s, + /* destroy= */ NULL, &f)); + ASSERT_OK_POSITIVE(sd_event_run(outer, 0)); + ASSERT_EQ(sd_future_state(f), SD_FUTURE_PENDING); + if (!timeout) + ASSERT_OK(sd_future_cancel(f)); + + while (sd_future_state(f) == SD_FUTURE_PENDING) + ASSERT_OK_POSITIVE(sd_event_run(outer, USEC_INFINITY)); + ASSERT_EQ(sd_future_result(f), timeout ? -ETIME : -ECANCELED); + ASSERT_EQ(count, 0); + + /* Cleanup must remove the outer loop's IO watcher and optional timer. Making + * the inner fd readable must not dispatch anything on the outer loop, while + * the inner loop itself remains usable after the interrupted run. */ + ASSERT_OK_EQ_ERRNO(write(pipefd[1], "X", 1), 1); + ASSERT_OK_ZERO(sd_event_run(outer, 0)); + ASSERT_OK_POSITIVE(sd_event_run(inner, 0)); + ASSERT_EQ(count, 1); + + int inner_fd = ASSERT_OK(sd_event_get_fd(inner)); + int outer_fd = ASSERT_OK(sd_event_get_fd(outer)); + f = sd_future_unref(f); + source = sd_event_source_unref(source); + inner = sd_event_unref(inner); + outer = sd_event_unref(outer); + + /* A leaked future or slot would keep its event loop and epoll fd alive. */ + ASSERT_ERROR_ERRNO(fcntl(inner_fd, F_GETFD), EBADF); + ASSERT_ERROR_ERRNO(fcntl(outer_fd, F_GETFD), EBADF); + } +} + DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/libsystemd/sd-future/fiber-io.c b/src/libsystemd/sd-future/fiber-io.c index 9fe0acfccd..b5c1f31245 100644 --- a/src/libsystemd/sd-future/fiber-io.c +++ b/src/libsystemd/sd-future/fiber-io.c @@ -10,7 +10,6 @@ #include "sd-event.h" #include "sd-future.h" -#include "alloc-util.h" #include "errno-util.h" #include "event-future.h" #include "fd-util.h" @@ -51,7 +50,7 @@ static ssize_t fiber_io_operation( if (r < 0) return r; - r = sd_fiber_suspend(); + r = sd_fiber_await(io); if (r < 0) return r; @@ -230,7 +229,7 @@ int sd_fiber_connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) /* future_new_io resolves with the revents mask on success; translate any positive value * (e.g. POLLOUT) back to the connect(2) success status. */ - r = sd_fiber_suspend(); + r = sd_fiber_await(io); return r > 0 ? 0 : r; } @@ -395,17 +394,18 @@ int sd_fiber_ppoll(struct pollfd *fds, size_t n_fds, const struct timespec *time if (zero_timeout || r != 0) /* Either error or some fds are ready */ return r; - sd_future **futures = NULL; - CLEANUP_ARRAY(futures, n_fds, sd_future_cancel_wait_unref_array); + /* Use a WAIT_ANY group: the first child (an fd readiness or the timer) to settle resolves + * the group, which cancels its siblings on the spot. The user-supplied event mask is in + * poll() bits (struct pollfd), so translate to epoll bits. */ + _cleanup_(sd_future_cancel_wait_unrefp) sd_future *group = NULL; + r = sd_future_group_new(e, &group); + if (r < 0) + return r; - futures = new0(sd_future*, n_fds); - if (!futures) - return -ENOMEM; + r = sd_future_group_set_policy(group, SD_FUTURE_GROUP_WAIT_ANY); + if (r < 0) + return r; - /* Set up I/O event sources for all valid fds. POLL* and EPOLL* share their bit values (see - * EPOLL_POLL_COMMON_MASK in io-util.h), so we can pass the user-supplied event mask through - * to either backend without translation. */ - size_t n_io_futures = 0; for (size_t i = 0; i < n_fds; i++) { if (fds[i].fd < 0) continue; @@ -414,11 +414,9 @@ int sd_fiber_ppoll(struct pollfd *fds, size_t n_fds, const struct timespec *time if (events == 0) continue; - r = future_new_io(e, fds[i].fd, events, &futures[i]); + r = future_group_add_io(group, fds[i].fd, events); if (r < 0) return r; - - n_io_futures++; } /* A timeout that overflows usec_t saturates to USEC_INFINITY in timespec_load(); treat that @@ -430,23 +428,21 @@ int sd_fiber_ppoll(struct pollfd *fds, size_t n_fds, const struct timespec *time /* If every fd was skipped (negative or empty event mask) and we'd have no timer, there's * nothing that could ever wake the fiber up — same situation as n_fds == 0 && !timeout, * just not detectable upfront. Refuse rather than suspend forever. */ - if (n_io_futures == 0 && usec == USEC_INFINITY) + if (sd_future_group_size(group) == 0 && usec == USEC_INFINITY) return -EINVAL; - _cleanup_(sd_future_cancel_wait_unrefp) sd_future *timer = NULL; if (usec != USEC_INFINITY) { - r = future_new_time_relative( - e, + r = future_group_add_time_relative( + group, CLOCK_MONOTONIC, usec, /* accuracy= */ 1, - /* result= */ 0, - &timer); + /* result= */ 0); if (r < 0) return r; } - r = sd_fiber_suspend(); + r = sd_fiber_await(group); if (r < 0 && r != -ETIME) return r; @@ -457,14 +453,10 @@ int sd_fiber_ppoll(struct pollfd *fds, size_t n_fds, const struct timespec *time if (n != 0) return n; - /* No fds ready: distinguish our own timer from an external -ETIME. */ - if (timer && sd_future_state(timer) == SD_FUTURE_RESOLVED) - return 0; - - /* An IO future resolved with a revents mask (r > 0) but the readiness was already consumed - * by the time we swept — report 0 rather than leaking the bitmask as a (bogus) ppoll fd - * count to the caller. */ - if (r > 0) + /* No fds ready. The group's result is the winning child's result: 0 means the timer + * (created with result=0) fired; r > 0 means an IO future fired (revents mask) but the + * readiness was already drained when we swept. Both map to "0 fds ready". */ + if (r >= 0) return 0; return r; diff --git a/src/libsystemd/sd-future/fiber.c b/src/libsystemd/sd-future/fiber.c index 64dee8411d..19075c68fa 100644 --- a/src/libsystemd/sd-future/fiber.c +++ b/src/libsystemd/sd-future/fiber.c @@ -39,13 +39,13 @@ * synonym there. */ _noreturn_ extern void siglongjmp_unchecked(sigjmp_buf env, int val) __asm__("siglongjmp"); -static thread_local Fiber *current_fiber = NULL; +static thread_local sd_future *current_fiber = NULL; typedef enum FiberState { FIBER_STATE_INITIAL, FIBER_STATE_READY, + FIBER_STATE_RUNNING, FIBER_STATE_SUSPENDED, - FIBER_STATE_CANCELLED, FIBER_STATE_COMPLETED, _FIBER_STATE_MAX, _FIBER_STATE_INVALID = -EINVAL, @@ -64,10 +64,11 @@ typedef struct Fiber { FiberState state; int result; /* Either resume error code or final return value */ + bool result_pending; /* sd_fiber_resume() stashed a value that fiber_swap() hasn't consumed yet */ sd_future *floating; /* Self-ref held while the fiber is floating; dropped on resolve. */ + sd_future *awaiting; /* Target of the wait the fiber is suspended in, if any. */ - sd_event *event; sd_event_source *defer_event_source; sd_event_source *exit_event_source; @@ -89,11 +90,15 @@ typedef struct Fiber { #endif } Fiber; -static Fiber* fiber_get_current(void) { - return current_fiber; +static Fiber* fiber_get(sd_future *f) { + return ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); } -static void fiber_set_current(Fiber *f) { +static Fiber* fiber_get_maybe(sd_future *f) { + return f ? fiber_get(f) : NULL; +} + +static void fiber_set_current(sd_future *f) { current_fiber = f; } @@ -167,7 +172,7 @@ static inline void finish_switch_stack(void *fake_stack_save) { /* Refresh f->resume_stack from whoever is currently the running fiber, so the next siglongjmp() out * of f (in the trampoline or fiber_swap()) can hand the right destination stack to ASAN. Must be - * called before fiber_set_current(f) — relies on fiber_get_current() returning the caller. */ + * called before fiber_set_current(f) — relies on sd_fiber_get_current() returning the caller. */ static void fiber_set_resume_stack(Fiber *f, Fiber *resume) { assert(f); @@ -178,70 +183,70 @@ static void fiber_set_resume_stack(Fiber *f, Fiber *resume) { } _noreturn_ static void fiber_entry_point(void) { - Fiber *f = ASSERT_PTR(fiber_get_current()); + Fiber *fiber = fiber_get(sd_fiber_get_current()); void *fake_stack_save = NULL; - assert(f->func); - assert(IN_SET(f->state, FIBER_STATE_INITIAL, FIBER_STATE_READY, FIBER_STATE_CANCELLED)); + assert(fiber->func); + assert(fiber->state == FIBER_STATE_INITIAL); finish_switch_stack(NULL); /* Capture our resumable point on the fiber's stack, then bounce back to whoever last set - * f->resume_context. On bootstrap that's fiber_bootstrap(); on every subsequent yield it's + * fiber->resume_context. On bootstrap that's fiber_init(); on every subsequent yield it's * the most recent fiber_run(). sigsetjmp(buf, 0) skips the signal-mask save: switching is * thread-shared with respect to signal masks. */ - if (sigsetjmp(f->context, /* savemask= */ 0) == 0) { - start_switch_stack(&fake_stack_save, &f->resume_stack); - siglongjmp_unchecked(f->resume_context, /* val= */ 1); + if (sigsetjmp(fiber->context, /* savemask= */ 0) == 0) { + start_switch_stack(&fake_stack_save, &fiber->resume_stack); + siglongjmp_unchecked(fiber->resume_context, /* val= */ 1); } - /* Re-entered for real via fiber_run()'s siglongjmp(f->context). */ + /* Re-entered for real via fiber_run()'s siglongjmp(fiber->context). */ finish_switch_stack(fake_stack_save); + assert(fiber->state == FIBER_STATE_RUNNING); /* Block scope so the cleanups attached to LOG_SET_PREFIX / LOG_CONTEXT_PUSH_KEY_VALUE fire * before the siglongjmp below — siglongjmp skips _cleanup_ attributes, so we have to make * sure the scope ends via a normal control-flow path first. */ { - LOG_SET_PREFIX(f->name); - LOG_CONTEXT_PUSH_KEY_VALUE("FIBER=", f->name); + LOG_SET_PREFIX(fiber->name); + LOG_CONTEXT_PUSH_KEY_VALUE("FIBER=", fiber->name); - f->result = f->state == FIBER_STATE_CANCELLED ? -ECANCELED : f->func(f->userdata); - f->state = FIBER_STATE_COMPLETED; + fiber->result = fiber->func(fiber->userdata); + fiber->state = FIBER_STATE_COMPLETED; } /* Pass NULL fake_stack_save to discard the fiber's fake stack since the fiber is done. */ - start_switch_stack(NULL, &f->resume_stack); + start_switch_stack(NULL, &fiber->resume_stack); /* Bounce back to whichever fiber_run() call most recently entered us. resume_context is * per-fiber so nested fiber_run() — e.g. a bus method dispatched as a fiber handler while * sd_event_loop() itself runs in a fiber — is safe. */ - siglongjmp_unchecked(f->resume_context, 1); + siglongjmp_unchecked(fiber->resume_context, 1); assert_not_reached(); } -static int fiber_init(Fiber *f) { +static int fiber_init(sd_future *f) { + Fiber *fiber = fiber_get(f); ucontext_t old_uc, uc; void *fake_stack_save = NULL; - assert(f); - if (getcontext(&uc) < 0) return -errno; - struct iovec fiber_stack = fiber_stack_usable(&f->stack); + struct iovec fiber_stack = fiber_stack_usable(&fiber->stack); uc.uc_link = NULL; /* Unused: trampoline siglongjmps out instead of returning. */ uc.uc_stack.ss_sp = fiber_stack.iov_base; uc.uc_stack.ss_size = fiber_stack.iov_len; uc.uc_stack.ss_flags = 0; - Fiber *prev = fiber_get_current(); + sd_future *prev = sd_fiber_get_current(); fiber_set_current(f); makecontext(&uc, fiber_entry_point, /* argc= */ 0); - fiber_set_resume_stack(f, prev); - if (sigsetjmp(f->resume_context, /* savemask= */ 0) == 0) { + fiber_set_resume_stack(fiber, fiber_get_maybe(prev)); + if (sigsetjmp(fiber->resume_context, /* savemask= */ 0) == 0) { start_switch_stack(&fake_stack_save, &fiber_stack); if (swapcontext(&old_uc, &uc) < 0) { finish_switch_stack(fake_stack_save); @@ -270,20 +275,23 @@ static void reset_current_fiber(void) { /* Restore the caller's log state stashed in the running fiber (if any) before clearing * current_fiber. Without this, the child of a fork() that happened mid-fiber would inherit the * fiber's log prefix / context list in its thread-locals even though no fiber is running. */ - Fiber *f = fiber_get_current(); + sd_future *f = sd_fiber_get_current(); if (f) { - fiber_swap_log_state(f); + Fiber *fiber = fiber_get(f); + fiber_swap_log_state(fiber); fiber_ops_set(NULL); } fiber_set_current(NULL); } -static sd_event_source* fiber_current_event_source(Fiber *f) { - assert(f); - assert(f->state != FIBER_STATE_COMPLETED); - assert(f->event); +static sd_event_source* fiber_current_event_source(sd_future *f) { + Fiber *fiber = fiber_get(f); + assert(fiber->state != FIBER_STATE_COMPLETED); - return sd_event_get_state(f->event) == SD_EVENT_EXITING ? f->exit_event_source : f->defer_event_source; + /* SD_EVENT_EXITING only covers the exit callback itself, not the time between callbacks or + * io_uring completions dispatched while exiting. The exit request persists across all of these. */ + return sd_event_get_exit_code(sd_future_get_event(f), /* ret= */ NULL) >= 0 ? + fiber->exit_event_source : fiber->defer_event_source; } static int atfork_ret; @@ -298,14 +306,12 @@ static void install_atfork(void) { } static void fiber_resolve(sd_future *f) { - Fiber *fiber = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); + Fiber *fiber = fiber_get(f); fiber->defer_event_source = sd_event_source_disable_unref(fiber->defer_event_source); fiber->exit_event_source = sd_event_source_disable_unref(fiber->exit_event_source); - /* The floating self-ref (if any) is potentially the last ref keeping the fiber alive — moving it - * into a local _cleanup_ slot ensures sd_future_resolve() runs callbacks and waiters while f is - * still valid; the local's cleanup drops the ref afterwards, at which point no further f->... - * access can happen. */ + /* The floating reference may be the last one. Keep it until sd_future_resolve() has marked the + * future resolved and armed its callbacks; dropping it can free the fiber immediately. */ _unused_ _cleanup_(sd_future_unrefp) sd_future *floating = TAKE_PTR(fiber->floating); sd_future_resolve(f, fiber->result); } @@ -318,18 +324,23 @@ static const FiberOps fiber_ops = { .cancel_wait_unref = sd_future_cancel_wait_unref, }; -static void fiber_enter(Fiber *fiber, Fiber *prev, void **fake_stack_save) { - fiber_set_current(fiber); +static void fiber_enter(sd_future *f, sd_future *prev, void **fake_stack_save) { + Fiber *fiber = fiber_get(f); + Fiber *prev_fiber = fiber_get_maybe(prev); + + fiber_set_current(f); fiber_swap_log_state(fiber); if (!prev) fiber_ops_set(&fiber_ops); struct iovec fiber_stack = fiber_stack_usable(&fiber->stack); start_switch_stack(fake_stack_save, &fiber_stack); - fiber_set_resume_stack(fiber, prev); + fiber_set_resume_stack(fiber, prev_fiber); } -static void fiber_leave(Fiber *fiber, Fiber *prev, void *fake_stack_save) { +static void fiber_leave(sd_future *f, sd_future *prev, void *fake_stack_save) { + Fiber *fiber = fiber_get(f); + finish_switch_stack(fake_stack_save); if (!prev) fiber_ops_set(NULL); @@ -338,13 +349,13 @@ static void fiber_leave(Fiber *fiber, Fiber *prev, void *fake_stack_save) { } static int fiber_run(sd_future *f) { - Fiber *fiber = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); + Fiber *fiber = fiber_get(f); int r; if (fiber->state == FIBER_STATE_COMPLETED) return -ESTALE; - assert(IN_SET(fiber->state, FIBER_STATE_INITIAL, FIBER_STATE_READY, FIBER_STATE_CANCELLED)); + assert(IN_SET(fiber->state, FIBER_STATE_INITIAL, FIBER_STATE_READY)); static pthread_once_t atfork_once = PTHREAD_ONCE_INIT; r = pthread_once(&atfork_once, install_atfork); @@ -362,18 +373,23 @@ static int fiber_run(sd_future *f) { * completes. This matters when fiber_run() is invoked from within another fiber (e.g. an * sd-event dispatch that happens to be running inside a fiber context itself): the * LOG_SET_PREFIX/LOG_CONTEXT_PUSH above attached to whichever fiber was current at that moment, - * and their scope-level cleanup must see the same fiber_get_current() when it runs to detach + * and their scope-level cleanup must see the same sd_fiber_get_current() when it runs to detach * them from the correct list. */ - Fiber *prev = fiber_get_current(); + sd_future *prev = sd_fiber_get_current(); void *fake_stack_save = NULL; - fiber_enter(fiber, prev, &fake_stack_save); + + /* INITIAL means the function has never started; READY means it yielded or was woken. Neither + * includes execution: stay RUNNING until a real yield, suspension, or completion, even while + * a nested event loop dispatches another fiber and temporarily changes the current fiber. */ + fiber->state = FIBER_STATE_RUNNING; + fiber_enter(f, prev, &fake_stack_save); /* This is where we start executing the fiber. Once it yields, we continue here as if nothing * happened. resume_context captures this point; the fiber siglongjmps back to it. */ if (sigsetjmp(fiber->resume_context, 0) == 0) siglongjmp_unchecked(fiber->context, 1); - fiber_leave(fiber, prev, fake_stack_save); + fiber_leave(f, prev, fake_stack_save); switch (fiber->state) { @@ -386,11 +402,10 @@ static int fiber_run(sd_future *f) { fiber_resolve(f); break; - case FIBER_STATE_CANCELLED: case FIBER_STATE_READY: log_debug("Fiber yielded execution"); - r = sd_event_source_set_enabled(fiber_current_event_source(fiber), SD_EVENT_ONESHOT); + r = sd_event_source_set_enabled(fiber_current_event_source(f), SD_EVENT_ONESHOT); if (r < 0) return r; break; @@ -408,33 +423,34 @@ static int fiber_run(sd_future *f) { } static int fiber_cancel(sd_future *f) { - Fiber *fiber = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); + Fiber *fiber = fiber_get(f); int r; - assert(fiber != fiber_get_current()); + assert(f != sd_fiber_get_current()); - if (IN_SET(fiber->state, FIBER_STATE_COMPLETED, FIBER_STATE_CANCELLED)) + if (fiber->state == FIBER_STATE_COMPLETED) return 0; if (fiber->state == FIBER_STATE_INITIAL) { - /* The fiber's stack was allocated but never entered, so there are no scope-level cleanups - * waiting to run. Skip the dispatch round-trip that would just have fiber_entry_point() - * fall straight through with -ECANCELED, and settle the future right here — mirroring the - * FIBER_STATE_COMPLETED branch of fiber_run(). */ + /* The fiber's function has never started, so there are no scope-level cleanups + * waiting to run. Skip the dispatch round-trip and settle the future right here — + * mirroring the FIBER_STATE_COMPLETED branch of fiber_run(). */ fiber->result = -ECANCELED; fiber->state = FIBER_STATE_COMPLETED; fiber_resolve(f); return 1; } - /* Once we cancel a fiber, we want to immediately resume it with -ECANCELED. */ - r = sd_event_source_set_enabled(fiber_current_event_source(fiber), SD_EVENT_ONESHOT); + bool queued = fiber->result_pending && fiber->result == -ECANCELED; + + /* Even an already-queued cancellation may need to move a pending defer dispatch to the exit + * source. Let sd_fiber_resume() handle scheduling in both cases, while keeping cancellation + * idempotent. -ECANCELED is sticky, so an async wakeup cannot overwrite it. */ + r = sd_fiber_resume(f, -ECANCELED); if (r < 0) return r; - fiber->state = FIBER_STATE_CANCELLED; - - return 1; + return !queued; } static int fiber_on_defer(sd_event_source *s, void *userdata) { @@ -444,21 +460,25 @@ static int fiber_on_defer(sd_event_source *s, void *userdata) { static int fiber_on_exit(sd_event_source *s, void *userdata) { sd_future *f = ASSERT_PTR(userdata); - Fiber *fiber = ASSERT_PTR(sd_future_get_private(f)); + Fiber *fiber = fiber_get(f); int r; - /* The fiber may already have completed via the regular defer path before sd_event_exit() - * fires the exit source; in that case there's nothing left to drive and we'd otherwise - * trip fiber_run()'s -ESTALE return, which sd_event would log spuriously and disable the - * source for. */ - if (fiber->state == FIBER_STATE_COMPLETED) - return 0; + /* An INITIAL fiber is cancelled synchronously; a COMPLETED fiber needs no further work. + * Return directly: cancelling an unstarted floating fiber may drop its last self-reference, + * so neither f nor fiber may be accessed afterwards. */ + if (IN_SET(fiber->state, FIBER_STATE_INITIAL, FIBER_STATE_COMPLETED)) + return fiber_cancel(f); - /* If fiber_cancel() returned 1 the fiber was just marked cancelled and its deferred/exit event - * source was re-armed; we let the event loop dispatch that source on the next iteration so it goes - * through the normal fiber_on_defer/fiber_on_exit path rather than running it recursively here. */ + /* Cancellation of a started fiber only queues an interruption; it cannot resolve it here. */ r = fiber_cancel(f); - if (r != 0) + if (r < 0) + return r; + + /* Run directly in this dispatch. Cancellation re-arms the source for a READY or SUSPENDED + * fiber; clear that redundant dispatch so cleanup can suspend without being run again + * prematurely. fiber_run() will re-arm the source itself if the fiber yields. */ + r = sd_event_source_set_enabled(s, SD_EVENT_OFF); + if (r < 0) return r; return fiber_run(f); @@ -469,7 +489,7 @@ static void* fiber_alloc(void) { } static void fiber_free(sd_future *f) { - Fiber *fiber = ASSERT_PTR(sd_future_get_private(f)); + Fiber *fiber = fiber_get(f); /* To make sure all memory is deallocated, the fiber has to have completed by the time we free it to * make sure its stack has finished unwinding (which will invoke the registered cleanup functions). @@ -480,7 +500,7 @@ static void fiber_free(sd_future *f) { * outer fiber should take care of cleaning up any created child fibers (for example using * sd_future_cancel_wait_unref()). * - * FIBER_STATE_INITIAL is also accepted: the stack was allocated but never entered, so there are no + * FIBER_STATE_INITIAL is also accepted: the function has never started, so there are no * registered cleanups to run. This covers the partial-construction failure path in sd_fiber_new() * as well as fibers that are unrefed before the event loop ever dispatches them. */ assert(IN_SET(fiber->state, FIBER_STATE_INITIAL, FIBER_STATE_COMPLETED)); @@ -498,79 +518,81 @@ static void fiber_free(sd_future *f) { sd_event_source_disable_unref(fiber->defer_event_source); sd_event_source_disable_unref(fiber->exit_event_source); - sd_event_unref(fiber->event); free(fiber->name); free(fiber); } sd_future* sd_fiber_get_current(void) { - Fiber *f = fiber_get_current(); - if (!f) - return NULL; - - return sd_event_source_get_userdata(fiber_current_event_source(f)); + return current_fiber; } int sd_fiber_is_running(void) { - return !!fiber_get_current(); + return !!current_fiber; } sd_event* sd_fiber_get_event(void) { - Fiber *f = fiber_get_current(); + sd_future *f = sd_fiber_get_current(); assert_return(f, NULL); - return f->event; + return sd_future_get_event(f); } int sd_fiber_get_priority(int64_t *ret) { - Fiber *f = fiber_get_current(); + sd_future *f = sd_fiber_get_current(); assert_return(ret, -EINVAL); assert_return(f, -ESRCH); - *ret = f->priority; + Fiber *fiber = fiber_get(f); + *ret = fiber->priority; return 0; } static int fiber_swap(FiberState state) { - Fiber *f = ASSERT_PTR(fiber_get_current()); + Fiber *fiber = fiber_get(sd_fiber_get_current()); - f->state = state; + assert(fiber->state == FIBER_STATE_RUNNING); + assert(IN_SET(state, FIBER_STATE_READY, FIBER_STATE_SUSPENDED)); - void *fake_stack_save = NULL; + /* A value queued by sd_fiber_resume() while the fiber was running short-circuits the swap: + * deliver it as if we had suspended and been resumed instantly, without round-tripping + * through the event loop. Neither yield nor suspend changes the scheduling state in this case: + * the caller must first observe the queued result before parking or yielding again. */ + if (!fiber->result_pending) { + fiber->state = state; - if (sigsetjmp(f->context, 0) == 0) { - start_switch_stack(&fake_stack_save, &f->resume_stack); - siglongjmp_unchecked(f->resume_context, 1); + void *fake_stack_save = NULL; + + if (sigsetjmp(fiber->context, 0) == 0) { + start_switch_stack(&fake_stack_save, &fiber->resume_stack); + siglongjmp_unchecked(fiber->resume_context, 1); + } + + finish_switch_stack(fake_stack_save); } - finish_switch_stack(fake_stack_save); + assert(fiber->state == FIBER_STATE_RUNNING); - /* When we get here, we've been resumed. */ - - if (f->state == FIBER_STATE_CANCELLED) - return -ECANCELED; - - /* sd_fiber_resume() stashes the resumer's value (an async wakeup error from a deadline - * timer, an io_uring CQE result, etc.) into f->result for us to surface here. Consume it - * unconditionally so it doesn't pollute subsequent suspends or the fiber's eventual return - * value — both negative errors and positive payloads (byte counts, accepted fds, revents - * masks) are valid resume values. */ - return TAKE_GENERIC(f->result, int, 0); + /* Consume the resume value after either path, whether it was queued while running or + * delivered after a context switch. Clear it unconditionally so it doesn't pollute + * subsequent suspends or the fiber's eventual return value — both negative errors and + * positive payloads (byte counts, accepted fds, revents masks) are valid resume values. */ + fiber->result_pending = false; + return TAKE_GENERIC(fiber->result, int, 0); } int sd_fiber_yield(void) { - assert_return(fiber_get_current(), -ESRCH); + assert_return(sd_fiber_get_current(), -ESRCH); return fiber_swap(FIBER_STATE_READY); } int sd_fiber_suspend(void) { - assert_return(fiber_get_current(), -ESRCH); + assert_return(sd_fiber_get_current(), -ESRCH); return fiber_swap(FIBER_STATE_SUSPENDED); } static int fiber_set_priority(sd_future *f, int64_t priority) { - Fiber *fiber = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); + Fiber *fiber = fiber_get(f); int r = 0; if (fiber->defer_event_source) @@ -588,22 +610,49 @@ static int fiber_set_priority(sd_future *f, int64_t priority) { static const sd_future_ops fiber_future_ops; int sd_fiber_resume(sd_future *f, int result) { + int r; + assert_return(f, -EINVAL); assert_return(sd_future_get_ops(f) == &fiber_future_ops, -EINVAL); - Fiber *fiber = ASSERT_PTR(sd_future_get_private(f)); + Fiber *fiber = fiber_get(f); - if (fiber->state != FIBER_STATE_SUSPENDED) + if (fiber->state == FIBER_STATE_COMPLETED) return 0; - /* Stash the result so fiber_swap() returns it from sd_fiber_suspend(). */ + /* Cancellation outranks timeout; neither may be overwritten by an ordinary wakeup. + * Make sure we always schedule below as a retained result may need to be moved from the + * defer source to the exit source on event loop exit. */ + if (fiber->result_pending && + (fiber->result == -ECANCELED || (fiber->result == -ETIME && result != -ECANCELED))) + result = fiber->result; + fiber->result = result; + fiber->result_pending = true; + + /* INITIAL already has a dispatch queued. RUNNING fibers (including active ancestors in nested + * loops) consume the result at their next fiber_swap(), without needing another dispatch. */ + if (IN_SET(fiber->state, FIBER_STATE_INITIAL, FIBER_STATE_RUNNING)) + return 0; + + assert(IN_SET(fiber->state, FIBER_STATE_READY, FIBER_STATE_SUSPENDED)); + + /* READY may need moving from defer source to exit source scheduling. Arm before changing state + * so a failure cannot leave a suspended fiber marked READY without a dispatch. */ + sd_event_source *source = fiber_current_event_source(f); + r = sd_event_source_set_enabled(source, SD_EVENT_ONESHOT); + if (r < 0) + return r; + fiber->state = FIBER_STATE_READY; - return sd_event_source_set_enabled(fiber_current_event_source(fiber), SD_EVENT_ONESHOT); + if (source == fiber->exit_event_source) + return sd_event_source_set_enabled(fiber->defer_event_source, SD_EVENT_OFF); + + return 0; } -/* The fiber_future ops pass the Fiber pointer through as the future's private state. The fiber resolves - * its own future once it finishes running, so fiber_cancel() intentionally does not resolve. */ +/* The fiber_future ops pass the Fiber pointer through as the future's private state. Once started, the + * fiber resolves its own future when it finishes running; cancellation only queues an interruption. */ static const sd_future_ops fiber_future_ops = { .size = sizeof(sd_future_ops), .alloc = fiber_alloc, @@ -622,12 +671,12 @@ int sd_fiber_new(sd_event *e, const char *name, sd_fiber_func_t func, void *user if (IN_SET(sd_event_get_state(e), SD_EVENT_EXITING, SD_EVENT_FINISHED)) return -ECANCELED; - _cleanup_(sd_future_unrefp) sd_future *f = NULL; - r = sd_future_new(&fiber_future_ops, &f); + _cleanup_(sd_future_cancel_unrefp) sd_future *f = NULL; + r = sd_future_new(e, &fiber_future_ops, &f); if (r < 0) return r; - Fiber *fiber = ASSERT_PTR(sd_future_get_private(f)); + Fiber *fiber = fiber_get(f); struct rlimit rl = { RLIM_INFINITY, RLIM_INFINITY }; if (getrlimit(RLIMIT_STACK, &rl) < 0) @@ -647,7 +696,6 @@ int sd_fiber_new(sd_event *e, const char *name, sd_fiber_func_t func, void *user .name = strdup(name), .func = func, .userdata = userdata, - .event = sd_event_ref(e), }; if (!fiber->name) return -ENOMEM; @@ -665,7 +713,7 @@ int sd_fiber_new(sd_event *e, const char *name, sd_fiber_func_t func, void *user (uint8_t*) usable.iov_base + usable.iov_len); #endif - r = fiber_init(fiber); + r = fiber_init(f); if (r < 0) return r; @@ -708,6 +756,9 @@ int sd_fiber_new(sd_event *e, const char *name, sd_fiber_func_t func, void *user r = sd_fiber_set_floating(f, true); if (r < 0) return r; + + /* Floating self-ref keeps the fiber alive; release our local ref without cancelling. */ + f = sd_future_unref(f); } /* We only take ownership of the given userdata pointer on success so assign the destroy callback @@ -721,7 +772,7 @@ int sd_fiber_set_floating(sd_future *f, int b) { assert_return(f, -EINVAL); assert_return(sd_future_get_ops(f) == &fiber_future_ops, -EINVAL); - Fiber *fiber = ASSERT_PTR(sd_future_get_private(f)); + Fiber *fiber = fiber_get(f); if (!!fiber->floating == !!b) return 0; @@ -741,12 +792,12 @@ int sd_fiber_get_floating(sd_future *f) { assert_return(f, -EINVAL); assert_return(sd_future_get_ops(f) == &fiber_future_ops, -EINVAL); - Fiber *fiber = ASSERT_PTR(sd_future_get_private(f)); + Fiber *fiber = fiber_get(f); return !!fiber->floating; } int sd_fiber_sleep(uint64_t usec) { - Fiber *f = fiber_get_current(); + sd_future *f = sd_fiber_get_current(); int r; if (!f) @@ -760,11 +811,9 @@ int sd_fiber_sleep(uint64_t usec) { if (usec == USEC_INFINITY) return sd_fiber_suspend(); - assert(f->event); - _cleanup_(sd_future_cancel_wait_unrefp) sd_future *timer = NULL; r = future_new_time_relative( - f->event, + sd_future_get_event(f), CLOCK_MONOTONIC, usec, /* accuracy= */ 1, @@ -773,7 +822,33 @@ int sd_fiber_sleep(uint64_t usec) { if (r < 0) return r; - return sd_fiber_suspend(); + return sd_fiber_await(timer); +} + +static int fiber_wait_callback(sd_future *target, void *userdata) { + /* Completion only wakes the waiter. Passing the future's error here would make it + * indistinguishable from an interruption of the waiting fiber. */ + return sd_fiber_resume(userdata, 0); +} + +static int fiber_suspend_for(sd_future *f, sd_future *target) { + Fiber *fiber = fiber_get(f); + int r; + + assert(!fiber->awaiting); + + fiber->awaiting = target; + r = sd_fiber_suspend(); + fiber->awaiting = NULL; + + return r; +} + +sd_future* sd_fiber_get_awaiting(sd_future *f) { + assert_return(f, NULL); + assert_return(sd_future_get_ops(f) == &fiber_future_ops, NULL); + + return fiber_get(f)->awaiting; } int sd_fiber_await(sd_future *target) { @@ -783,37 +858,120 @@ int sd_fiber_await(sd_future *target) { assert_return(f, -ESRCH); assert_return(target, -EINVAL); assert_return(target != f, -EDEADLK); - - Fiber *fiber = ASSERT_PTR(sd_future_get_private(f)); + assert_return(sd_future_get_event(target) == sd_future_get_event(f), -EINVAL); if (sd_future_state(target) == SD_FUTURE_RESOLVED) return sd_future_result(target); - /* Note that we do allow waiting for other fibers when the event loop is exiting, since waiting for - * other fibers does not require adding new event sources to the event loop. */ - if (sd_event_get_state(fiber->event) == SD_EVENT_FINISHED) - return -ECANCELED; + /* The fiber is executing inside event dispatch, so its loop cannot have finished yet. + * Waiting during exit is supported by the slot's exit source. */ + assert(sd_event_get_state(sd_future_get_event(f)) != SD_EVENT_FINISHED); - _cleanup_(sd_future_cancel_wait_unrefp) sd_future *wait = NULL; - r = sd_future_new_wait(target, &wait); + _cleanup_(sd_future_slot_unrefp) sd_future_slot *slot = NULL; + r = sd_future_add_callback(target, &slot, fiber_wait_callback, f); if (r < 0) return r; - return sd_fiber_suspend(); + r = fiber_suspend_for(f, target); + if (r < 0) + return r; + + if (sd_future_state(target) != SD_FUTURE_RESOLVED) + return -EBUSY; + + /* Read the result while the slot still pins the target: releasing it may drop the last ref. */ + return sd_future_result(target); +} + +sd_future* sd_future_cancel_wait_unref(sd_future *f) { + sd_future *self = sd_fiber_get_current(); + int r, q = 0; + + if (!f) + return NULL; + + /* Outside a fiber we can still clean up futures whose cancellation is synchronous, or which + * are already resolved. Asynchronous cancellation requires a fiber to await completion. */ + if (!self) + return sd_future_cancel_unref(f); + + /* Invalid calls still consume their reference. Releasing the last reference to a pending + * future remains a programming error, diagnosed by sd_future_free(). */ + assert_return(f != self, sd_future_unref(f)); + + for (;;) { + r = sd_future_cancel(f); + if (r < 0) + log_debug_errno(r, "Failed to cancel future, ignoring: %m"); + + if (sd_future_state(f) != SD_FUTURE_PENDING) + break; + + /* Synchronous cancellation needs no dispatch and works across event loops too. */ + assert_return(sd_future_get_event(f) == sd_future_get_event(self), sd_future_unref(f)); + + /* Unlike sd_fiber_await(), a failure to set up the wait must be told apart from a + * wait that was interrupted or woken spuriously. Retrying the former would spin + * without ever yielding to the event loop, so nothing could resolve the future. */ + _cleanup_(sd_future_slot_unrefp) sd_future_slot *slot = NULL; + r = sd_future_add_callback(f, &slot, fiber_wait_callback, self); + if (r < 0) { + log_debug_errno(r, "Failed to wait for future to finish, giving up: %m"); + break; + } + + r = fiber_suspend_for(self, f); + + if (sd_future_state(f) == SD_FUTURE_RESOLVED) { + int fr = sd_future_result(f); + if (fr < 0 && fr != -ECANCELED) + log_debug_errno(fr, "Future resolved with error, ignoring: %m"); + } + + if (IN_SET(r, -ECANCELED, -ETIME)) { + log_debug_errno(r, "Interrupted while waiting for future to finish, deferring the interruption until the future resolves: %m"); + /* The wait reports interruptions separately from the future's result. Preserve + * them even if the future resolved concurrently, or a previous cleanup queued + * the interruption before this wait. Cancellation takes precedence over timeout. */ + if (q != -ECANCELED) + q = r; + } else if (r < 0) + log_debug_errno(r, "Failed to wait for future to finish, ignoring: %m"); + + if (sd_future_state(f) != SD_FUTURE_PENDING) + break; + } + + /* Re-queue the interruption we held back, if any. */ + if (q < 0) { + r = sd_fiber_resume(self, q); + if (r < 0) + log_debug_errno(r, "Failed to re-queue interruption (%i) on calling fiber, ignoring: %m", q); + } + + return sd_future_unref(f); +} + +DEFINE_POINTER_ARRAY_CLEAR_FUNC(sd_future*, sd_future_cancel_wait_unref); +DEFINE_POINTER_ARRAY_FREE_FUNC(sd_future*, sd_future_cancel_wait_unref); + +static int fiber_timeout_callback(sd_future *f, void *userdata) { + /* Timeout scopes deliver -ETIME, unlike ordinary waits which only signal completion. */ + return sd_fiber_resume(userdata, sd_future_result(f)); } sd_future* sd_fiber_timeout(uint64_t timeout) { - Fiber *fiber = fiber_get_current(); + sd_future *f = sd_fiber_get_current(); int r; - assert_return(fiber, NULL); + assert_return(f, NULL); if (timeout == USEC_INFINITY) return NULL; - sd_future *timer; + _cleanup_(sd_future_cancel_wait_unrefp) sd_future *timer = NULL; r = future_new_time_relative( - fiber->event, + sd_future_get_event(f), CLOCK_MONOTONIC, timeout, /* accuracy= */ 1, @@ -823,5 +981,12 @@ sd_future* sd_fiber_timeout(uint64_t timeout) { return NULL; /* On allocation failure no timer is armed and the scope becomes a no-op. * Errors here are rare; if the caller cares they can compare to NULL. */ - return timer; + /* The whole point of SD_FIBER_TIMEOUT is to wake the calling fiber when the deadline + * fires (so a later sd_fiber_suspend / sd_fiber_await returns -ETIME from this timer's + * resolve). Install a floating resume callback bound to the timer's lifetime. */ + r = sd_future_add_callback(timer, /* ret_slot= */ NULL, fiber_timeout_callback, f); + if (r < 0) + return NULL; + + return TAKE_PTR(timer); } diff --git a/src/libsystemd/sd-future/future-group.c b/src/libsystemd/sd-future/future-group.c new file mode 100644 index 0000000000..a897e2003b --- /dev/null +++ b/src/libsystemd/sd-future/future-group.c @@ -0,0 +1,322 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "sd-future.h" + +#include "alloc-util.h" +#include "errno-util.h" +#include "macro.h" + +typedef struct FutureGroup { + uint64_t policy; + + /* An explicit priority applies to existing and future children. Otherwise preserve theirs. */ + int64_t priority; + bool priority_set; + + sd_future_slot **slots; + size_t n_slots; + + /* The fiber the group was created on, if it uses the same event loop. When the + * group settles on an error and IGNORE_ERRORS is unset, this fiber is cancelled so it + * notices the failure even if it hasn't started awaiting the group (a child error cancels + * the parent). parent_slot's callback NULLs `parent` if the parent resolves before the + * group does. */ + sd_future *parent; + sd_future_slot *parent_slot; + + /* Set once future_group_finalize() has been entered. The outcome is decided (stored in + * `result`) and the group is "draining" — waiting for any still-pending children to + * actually settle before we resolve. While set, the result cannot change and add + * rejects with -ESTALE. */ + bool finalizing; + int result; +} FutureGroup; + +static void* future_group_alloc(void) { + return new0(FutureGroup, 1); +} + +static void future_group_free(sd_future *f) { + FutureGroup *fg = ASSERT_PTR(sd_future_get_private(f)); + + sd_future_slot_unref(fg->parent_slot); + FOREACH_ARRAY(slot_p, fg->slots, fg->n_slots) + sd_future_slot_unref(*slot_p); + free(fg->slots); + free(fg); +} + +static int future_group_parent_resolved(sd_future *parent, void *userdata) { + FutureGroup *fg = ASSERT_PTR(userdata); + fg->parent = NULL; + fg->parent_slot = sd_future_slot_unref(fg->parent_slot); + return 0; +} + +static int future_group_check(sd_future *g); + +static int future_group_finalize(sd_future *g, int result, bool propagate_error) { + FutureGroup *fg = ASSERT_PTR(sd_future_get_private(g)); + int r = 0; + + if (fg->finalizing) + /* Outcome already locked: ignore subsequent attempts. Mirrors the old "group + * is already RESOLVED, so further cancels are no-ops" behaviour. */ + return 0; + + fg->finalizing = true; + fg->result = result; + + /* A cancellation error does not make a pending child safe to release. Keep waiting for its + * resolution even when cancellation is unsupported or fails; child implementations must still + * arrange completion before the group can finish draining. */ + FOREACH_ARRAY(slot_p, fg->slots, fg->n_slots) { + sd_future *child = sd_future_slot_get_future(*slot_p); + if (sd_future_state(child) == SD_FUTURE_PENDING) + RET_GATHER(r, sd_future_cancel(child)); + } + + /* If we're settling because of a child error (and the user hasn't opted into ignoring + * errors), cancel the parent fiber so it notices the failure even if it hasn't + * started awaiting the group yet. An active await means the parent will receive the + * group's actual error, so cancelling it would only hide that error behind -ECANCELED. + * The suppression ends automatically when an interrupted wait returns; a peer's wait + * cannot suppress cancellation of the parent. + * Explicit group cancellation never propagates upward. */ + if (propagate_error && result < 0 && + !FLAGS_SET(fg->policy, SD_FUTURE_GROUP_IGNORE_ERRORS) && + fg->parent && + fg->parent != sd_fiber_get_current() && + sd_fiber_get_awaiting(fg->parent) != g) + RET_GATHER(r, sd_future_cancel(fg->parent)); + + /* Re-check: if every child settled synchronously during the cancel loop the group can + * resolve now; otherwise wait for the group_child_resolved callbacks to drive the + * drain branch of check(). */ + RET_GATHER(r, future_group_check(g)); + return r; +} + +static int future_group_check(sd_future *g) { + FutureGroup *fg = ASSERT_PTR(sd_future_get_private(g)); + + if (sd_future_state(g) == SD_FUTURE_RESOLVED) + return 0; + + if (fg->finalizing) { + /* Outcome decided; resolve once every child has actually settled so callers + * observing the group's resolution see every child in RESOLVED state. An empty + * finalizing group resolves immediately (the FOREACH_ARRAY body never runs). */ + FOREACH_ARRAY(slot_p, fg->slots, fg->n_slots) + if (sd_future_state(sd_future_slot_get_future(*slot_p)) != SD_FUTURE_RESOLVED) + return 0; + return sd_future_resolve(g, fg->result); + } + + if (fg->n_slots == 0) + /* Empty group has nothing to wait for: leave it pending so the user can still + * add children (or cancel the group). Otherwise an early set_policy on a + * fresh group would settle it before any child got added. */ + return 0; + + bool wait_any = FLAGS_SET(fg->policy, SD_FUTURE_GROUP_WAIT_ANY); + bool ignore_errors = FLAGS_SET(fg->policy, SD_FUTURE_GROUP_IGNORE_ERRORS); + + size_t n_resolved = 0; + int first_error = 0, first_success = 0; + bool any_success = false; + + FOREACH_ARRAY(slot_p, fg->slots, fg->n_slots) { + sd_future *child = sd_future_slot_get_future(*slot_p); + if (sd_future_state(child) != SD_FUTURE_RESOLVED) + continue; + + n_resolved++; + int cr = sd_future_result(child); + if (cr < 0) { + if (first_error == 0) + first_error = cr; + } else if (!any_success) { + any_success = true; + first_success = cr; + } + } + + bool all_done = (n_resolved == fg->n_slots); + + int result; + if (wait_any && any_success) + result = first_success; /* wait_any short-circuits on first success */ + else if (!ignore_errors && first_error != 0) + result = first_error; /* fail-fast on error unless ignored */ + else if (all_done) + result = first_error; /* everyone settled: 0 if no errors */ + else + return 0; + + return future_group_finalize(g, result, /* propagate_error= */ true); +} + +static int future_group_cancel(sd_future *f) { + /* Explicit group cancellation affects its children, not the fiber that created the group. */ + return future_group_finalize(f, -ECANCELED, /* propagate_error= */ false); +} + +static int future_group_set_child_priority(sd_future *child, int64_t priority) { + int r; + + r = sd_future_set_priority(child, priority); + /* Some children do not support priorities or have already resolved. */ + if (r < 0 && !IN_SET(r, -EOPNOTSUPP, -ESTALE)) + return r; + + return 0; +} + +static int future_group_set_priority(sd_future *f, int64_t priority) { + FutureGroup *fg = ASSERT_PTR(sd_future_get_private(f)); + int r = 0; + + FOREACH_ARRAY(slot_p, fg->slots, fg->n_slots) + RET_GATHER(r, future_group_set_child_priority(sd_future_slot_get_future(*slot_p), priority)); + if (r < 0) + return r; + + fg->priority = priority; + fg->priority_set = true; + return 0; +} + +static const sd_future_ops future_group_ops = { + .size = sizeof(sd_future_ops), + .alloc = future_group_alloc, + .free = future_group_free, + .cancel = future_group_cancel, + .set_priority = future_group_set_priority, +}; + +int sd_future_group_new(sd_event *e, sd_future **ret) { + int r; + + assert_return(e, -EINVAL); + assert_return(ret, -EINVAL); + + _cleanup_(sd_future_cancel_unrefp) sd_future *g = NULL; + r = sd_future_new(e, &future_group_ops, &g); + if (r < 0) + return r; + + sd_future *parent = sd_fiber_get_current(); + if (parent && sd_future_get_event(parent) == sd_future_get_event(g)) { + FutureGroup *fg = sd_future_get_private(g); + r = sd_future_add_callback(parent, &fg->parent_slot, future_group_parent_resolved, fg); + if (r < 0) + return r; + fg->parent = parent; + } + + *ret = TAKE_PTR(g); + return 0; +} + +size_t sd_future_group_size(sd_future *f) { + if (!f) + return 0; + + assert_return(sd_future_get_ops(f) == &future_group_ops, 0); + + FutureGroup *fg = sd_future_get_private(f); + return fg->n_slots; +} + +int sd_future_group_set_policy(sd_future *f, uint64_t policy) { + assert_return(f, -EINVAL); + assert_return(sd_future_get_ops(f) == &future_group_ops, -EINVAL); + assert_return(sd_future_state(f) == SD_FUTURE_PENDING, -ESTALE); + assert_return((policy & ~(uint64_t) _SD_FUTURE_GROUP_POLICY_MASK) == 0, -EINVAL); + + /* Policy must be configured before any children are added — once a child is in flight, + * the resolution mechanics are locked in. This keeps the API friction-free: callers + * don't have to reason about mid-flight reshuffling of which children get cancelled. */ + FutureGroup *fg = sd_future_get_private(f); + assert_return(fg->n_slots == 0, -ESTALE); + + fg->policy = policy; + return 0; +} + +static int group_child_resolved(sd_future *child, void *userdata) { + sd_future *g = ASSERT_PTR(userdata); + return future_group_check(g); +} + +int sd_future_group_add(sd_future *f, sd_future *child) { + int r; + + assert_return(f, -EINVAL); + assert_return(child, -EINVAL); + assert_return(sd_future_get_ops(f) == &future_group_ops, -EINVAL); + assert_return(sd_future_state(f) == SD_FUTURE_PENDING, -ESTALE); + /* A group waiting for itself can never settle. */ + assert_return(child != f, -EINVAL); + + /* Child notifications are dispatched on the child's event loop. */ + assert_return(sd_future_get_event(child) == sd_future_get_event(f), -EINVAL); + + FutureGroup *fg = sd_future_get_private(f); + /* Group is draining: a freshly-added pending child would have missed the cancel loop + * and hang us forever waiting for it to settle. */ + assert_return(!fg->finalizing, -ESTALE); + + if (!GREEDY_REALLOC(fg->slots, fg->n_slots + 1)) + return -ENOMEM; + + _cleanup_(sd_future_slot_unrefp) sd_future_slot *slot = NULL; + r = sd_future_add_callback(child, &slot, group_child_resolved, f); + if (r < 0) + return r; + + if (fg->priority_set) { + r = future_group_set_child_priority(child, fg->priority); + if (r < 0) + return r; + } + + fg->slots[fg->n_slots++] = TAKE_PTR(slot); + + return 0; +} + +int sd_future_group_add_many_internal(sd_future *f, ...) { + assert_return(f, -EINVAL); + assert_return(sd_future_get_ops(f) == &future_group_ops, -EINVAL); + + FutureGroup *fg = sd_future_get_private(f); + size_t before = fg->n_slots; + int r = 0; + + va_list ap; + va_start(ap, f); + for (;;) { + sd_future *child = va_arg(ap, sd_future*); + if (!child) + break; + + r = sd_future_group_add(f, child); + if (r < 0) + break; + } + va_end(ap); + + if (r < 0) + /* No callbacks run inline while adding children, so the group cannot start finalizing + * during this call. Roll back only this call's additions. */ + while (fg->n_slots > before) { + sd_future_slot_unref(fg->slots[--fg->n_slots]); + fg->slots[fg->n_slots] = NULL; + } + + return r; +} diff --git a/src/libsystemd/sd-future/sd-future.c b/src/libsystemd/sd-future/sd-future.c index ba3a1c52d2..6278a64e86 100644 --- a/src/libsystemd/sd-future/sd-future.c +++ b/src/libsystemd/sd-future/sd-future.c @@ -1,130 +1,180 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "sd-event.h" #include "sd-future.h" #include "alloc-util.h" #include "errno-util.h" +#include "event-future.h" +#include "event-util.h" #include "log.h" #include "macro.h" #include "set.h" +struct sd_future_slot { + unsigned n_ref; + + /* Back-pointer to the future the slot is attached to. + * + * Ref ownership is asymmetric (same trick as sd_bus_slot/bus->slots): when the slot + * is non-floating the SLOT owns a ref on the future; when floating, the FUTURE owns + * a ref on the slot. So `slots` is always a borrowed pointer collection regardless. + * + * Consequence of the non-floating case: because the slot holds a ref, dropping the slot may + * perform the future's last unref — which, like any last unref, requires the future to already + * be RESOLVED. A caller that releases its future handle and relies on the slot to keep the + * future alive must drive it to resolution before dropping the slot. */ + sd_future *future; + bool floating; + + sd_future_func_t callback; + void *userdata; + + sd_event_source *defer_source; + sd_event_source *exit_source; +}; + struct sd_future { unsigned n_ref; int state; int result; - Set *waiters; + sd_event *event; - sd_future_func_t callback; - void *userdata; + Set *slots; const sd_future_ops *ops; /* Opaque per-future state owned by the future implementation (the code that called - * sd_future_new()). The ops vtable above receives this pointer in its callbacks, and - * external code can fetch it via sd_future_get_private(). */ + * sd_future_new()). The ops callbacks and external code access this state via + * sd_future_get_private(). */ void *private; }; -static int fiber_resume_trampoline(sd_future *f) { - /* The future's result is what the fiber should resume with. Impls choose the value at - * resolution time — e.g. a deadline timer resolves with -ETIME, a wait future resolves - * with the target's result, a normal IO/sleep future resolves with 0 on success. */ - return sd_fiber_resume(sd_future_get_userdata(f), sd_future_result(f)); +static int slot_dispatch_handler(sd_event_source *src, void *userdata) { + sd_future_slot *s = ASSERT_PTR(userdata); + sd_future *f = s->future; + int r; + + /* Invoked when the slot's chosen event source fires. Hold a self-ref on `f` + * across the callback: a floating slot lives in f->slots, so if the callback + * drops the last user-side ref to f, sd_future_free would iterate floating + * slots and unref us — pulling the rug out from under the `sd_future_slot_unref` + * below. Holding the ref keeps f alive until we've cleaned ourselves up; the + * cleanup at scope exit drops it, freeing f if no one else held a ref. */ + bool floating = s->floating; /* capture: non-floating s may be freed by callback */ + _unused_ _cleanup_(sd_future_unrefp) sd_future *self = sd_future_ref(f); + + /* Both sources are armed so a queued callback survives an exit request. Disable both before + * invoking it, since the callback may free the slot or itself request exit. Disabling defer + * and exit sources is non-failing during valid dispatch. */ + assert_se(sd_event_source_set_enabled(s->defer_source, SD_EVENT_OFF) >= 0); + assert_se(sd_event_source_set_enabled(s->exit_source, SD_EVENT_OFF) >= 0); + + r = s->callback(f, s->userdata); + if (floating) + sd_future_slot_unref(s); + return r; +} + +static int slot_arm(sd_future_slot *s) { + int r = 0; + + /* Exit may be requested after arming but before dispatching the callback. */ + RET_GATHER(r, sd_event_source_set_enabled(s->defer_source, SD_EVENT_ONESHOT)); + RET_GATHER(r, sd_event_source_set_enabled(s->exit_source, SD_EVENT_ONESHOT)); + if (r < 0) + return log_debug_errno(r, "Failed to arm future callback: %m"); + + return 0; } int sd_future_resolve(sd_future *f, int result) { int r = 0; assert_return(f, -EINVAL); - - if (f->state != SD_FUTURE_PENDING) - return 0; - - /* Hold a self-ref across callback/waiter dispatch: callbacks (e.g. bus_fiber_resolved() - * dropping the tracking-set's ref) may legitimately release what would otherwise be the - * last reference, and we still access f->waiters below. The cleanup unrefs at scope exit, - * which is when freeing is safe again. */ - _unused_ _cleanup_(sd_future_unrefp) sd_future *self = sd_future_ref(f); + assert_return(f->state == SD_FUTURE_PENDING, -ESTALE); f->state = SD_FUTURE_RESOLVED; f->result = result; - if (f->callback) - RET_GATHER(r, f->callback(f)); - - /* We'd like the set to not be modified while iterating over it, hence take ownership over it in - * a local variable. Otherwise code invoked via sd_future_resolve() could try to modify the set while - * we're iterating over it (for example wait_future_free()). */ - Set *waiters = TAKE_PTR(f->waiters); - sd_future *w; - SET_FOREACH(w, waiters) - RET_GATHER(r, sd_future_resolve(w, result)); - - set_free(waiters); + /* Enable each slot's event sources so the callback fires on the next loop iteration. Deferring + * callbacks frees the resolver from reentrancy concerns, slot-set mutation during iteration, + * and callbacks dropping the future's last ref. */ + sd_future_slot *s; + SET_FOREACH(s, f->slots) + RET_GATHER(r, slot_arm(s)); return r; } static sd_future* sd_future_free(sd_future *f) { - if (!f) - return NULL; + /* By the time we tear down, the future must have reached a terminal state. Callers + * abandoning a still-PENDING future must drive it to RESOLVED first — typically via + * sd_future_cancel_unref() (non-fiber, synchronous-cancel impls) or + * sd_future_cancel_wait_unref() (fiber, awaits actual resolution). */ + assert(f->state == SD_FUTURE_RESOLVED); - if (f->state == SD_FUTURE_PENDING) - sd_future_resolve(f, -ECANCELED); - - set_free(f->waiters); + /* Any slot still in f->slots at this point must be floating: non-floating slots own + * a ref on f, so if any existed we wouldn't have reached free. (Slots can be added + * post-resolution via sd_future_add_callback — those are floating and may not have + * had their defer tick yet.) Tear them down by dropping the future's ref. */ + Set *slots = TAKE_PTR(f->slots); + sd_future_slot *s; + SET_FOREACH(s, slots) { + assert(s->floating); + sd_future_slot_unref(s); + } + set_free(slots); if (f->ops->free) f->ops->free(f); + sd_event_unref(f->event); return mfree(f); } +/* Unref is a pure refcount op: dropping a ref does not resolve or cancel. If a caller wants + * pending work to complete (and floating callbacks to observe the outcome) before release, + * they must drive resolution explicitly — typically via sd_future_cancel() or + * sd_future_cancel_wait_unref(). Reaching sd_future_free() with state PENDING is a programming + * error and trips an assert: callers must drive the future to RESOLVED before dropping the last + * ref. */ DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_future, sd_future, sd_future_free); + DEFINE_POINTER_ARRAY_CLEAR_FUNC(sd_future*, sd_future_unref); DEFINE_POINTER_ARRAY_FREE_FUNC(sd_future*, sd_future_unref); -sd_future* sd_future_cancel_wait_unref(sd_future *f) { +sd_future* sd_future_cancel_unref(sd_future *f) { int r; if (!f) return NULL; - /* We have to be able to suspend until the fiber we're waiting for finishes, and that's only - * possible if we're running on a fiber ourselves. */ - if (!sd_fiber_is_running()) - return sd_future_unref(f); - + /* Synchronous-cancel teardown for non-fiber contexts: drive the future to RESOLVED via + * ops->cancel, then drop the ref. Safe for impls whose ops->cancel resolves synchronously. + * For impls whose cancel is asynchronous, the future stays PENDING after this call and + * sd_future_free's strict assert will trip. Callers in that situation must use + * sd_future_cancel_wait_unref() from a fiber to await the actual resolution. */ r = sd_future_cancel(f); if (r < 0) log_debug_errno(r, "Failed to cancel future, ignoring: %m"); - if (f->state == SD_FUTURE_PENDING) { - /* Fast path: when f's resolve callback already targets the current fiber (the default for - * futures created on this fiber), we can suspend directly and let the existing trampoline - * wake us up — no need to allocate a wait future just to learn about the resolution. - * Otherwise fall back to sd_fiber_await() which sets up an explicit waiter. */ - if (f->callback == fiber_resume_trampoline && f->userdata == sd_fiber_get_current()) - r = sd_fiber_suspend(); - else - r = sd_fiber_await(f); - if (r < 0 && r != -ECANCELED) - log_debug_errno(r, "Failed to wait for future to finish, ignoring: %m"); - } - return sd_future_unref(f); } -DEFINE_POINTER_ARRAY_CLEAR_FUNC(sd_future*, sd_future_cancel_wait_unref); -DEFINE_POINTER_ARRAY_FREE_FUNC(sd_future*, sd_future_cancel_wait_unref); +DEFINE_POINTER_ARRAY_CLEAR_FUNC(sd_future*, sd_future_cancel_unref); +DEFINE_POINTER_ARRAY_FREE_FUNC(sd_future*, sd_future_cancel_unref); -int sd_future_new(const sd_future_ops *ops, sd_future **ret) { +int sd_future_new(sd_event *e, const sd_future_ops *ops, sd_future **ret) { + assert_return(e, -EINVAL); + assert_return(e = event_resolve(e), -ENOPKG); assert_return(ops, -EINVAL); assert_return(ops->size >= endoffsetof_field(sd_future_ops, set_priority), -EINVAL); assert_return(ops->alloc, -EINVAL); assert_return(ops->free, -EINVAL); + assert_return(ops->cancel, -EINVAL); assert_return(ret, -EINVAL); sd_future *f = new(sd_future, 1); @@ -143,18 +193,16 @@ int sd_future_new(const sd_future_ops *ops, sd_future **ret) { return -ENOMEM; } - /* If we're being created on a fiber, default the callback to resuming that fiber on resolve — - * this is almost always what you want, and it saves the usual set_callback boilerplate before - * sd_fiber_suspend(). Callers that want different behavior can override with - * sd_future_set_callback(). */ - sd_future *fiber = sd_fiber_get_current(); - if (fiber) - (void) sd_future_set_callback(f, fiber_resume_trampoline, fiber); - + f->event = sd_event_ref(e); *ret = f; return 0; } +sd_event* sd_future_get_event(sd_future *f) { + assert_return(f, NULL); + return f->event; +} + int sd_future_state(sd_future *f) { assert_return(f, -EINVAL); return f->state; @@ -166,11 +214,6 @@ int sd_future_result(sd_future *f) { return f->result; } -void* sd_future_get_userdata(sd_future *f) { - assert_return(f, NULL); - return f->userdata; -} - void* sd_future_get_private(sd_future *f) { assert_return(f, NULL); return f->private; @@ -181,11 +224,102 @@ const sd_future_ops* sd_future_get_ops(sd_future *f) { return f->ops; } -int sd_future_set_callback(sd_future *f, sd_future_func_t callback, void *userdata) { - assert_return(f, -EINVAL); +sd_future* sd_future_slot_get_future(sd_future_slot *s) { + assert_return(s, NULL); + return s->future; +} - f->callback = callback; - f->userdata = userdata; +static sd_future_slot* sd_future_slot_free(sd_future_slot *s) { + if (!s) + return NULL; + + if (s->future) { + set_remove(s->future->slots, s); + if (!s->floating) { + /* A non-floating slot owns a ref on its future, so this may be the last unref. + * Like any last unref it requires the future to already be RESOLVED — dropping a + * slot must never be what abandons a still-PENDING future. Assert here so the + * misuse points at the slot drop rather than surfacing as the generic + * sd_future_free() assert. */ + assert(s->future->n_ref > 1 || s->future->state == SD_FUTURE_RESOLVED); + sd_future_unref(s->future); + } + } + + sd_event_source_disable_unref(s->defer_source); + sd_event_source_disable_unref(s->exit_source); + + return mfree(s); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_future_slot, sd_future_slot, sd_future_slot_free); + +static int slot_source_setup(sd_event_source *s) { + int r; + + r = sd_event_source_set_enabled(s, SD_EVENT_OFF); + if (r < 0) + return r; + + return event_source_inherit_fiber_priority(s); +} + +int sd_future_add_callback(sd_future *f, sd_future_slot **ret_slot, sd_future_func_t callback, void *userdata) { + int r; + + assert_return(f, -EINVAL); + assert_return(callback, -EINVAL); + + _cleanup_(sd_future_slot_unrefp) sd_future_slot *s = new(sd_future_slot, 1); + if (!s) + return -ENOMEM; + + *s = (sd_future_slot) { + .n_ref = 1, + .future = f, + .floating = ret_slot == NULL, + .callback = callback, + .userdata = userdata, + }; + + /* Asymmetric ownership (same trick as sd_bus_slot): non-floating slot owns the + * future, floating slot is owned by it — avoids a cycle in either direction. */ + if (!s->floating) + sd_future_ref(f); + + /* Never run the callback inline, even if the future is already resolved. Queue it for either + * regular dispatch or exit, so callers need not handle synchronous or missed notifications. */ + + r = sd_event_add_defer(f->event, &s->defer_source, slot_dispatch_handler, s); + if (r < 0) + return r; + + r = slot_source_setup(s->defer_source); + if (r < 0) + return r; + + r = sd_event_add_exit(f->event, &s->exit_source, slot_dispatch_handler, s); + if (r < 0) + return r; + + r = slot_source_setup(s->exit_source); + if (r < 0) + return r; + + if (f->state == SD_FUTURE_RESOLVED) { + r = slot_arm(s); + if (r < 0) + return r; + } + + r = set_ensure_put(&f->slots, &trivial_hash_ops, s); + if (r < 0) + return r; + + if (ret_slot) + *ret_slot = s; + + TAKE_PTR(s); return 0; } @@ -199,65 +333,9 @@ int sd_future_set_priority(sd_future *f, int64_t priority) { int sd_future_cancel(sd_future *f) { assert_return(f, -EINVAL); - assert_return(f->ops->cancel, -EOPNOTSUPP); if (f->state == SD_FUTURE_RESOLVED) return 0; return f->ops->cancel(f); } - -typedef struct WaitFuture { - sd_future *target; -} WaitFuture; - -static void* wait_future_alloc(void) { - return new0(WaitFuture, 1); -} - -static void wait_future_free(sd_future *f) { - WaitFuture *wf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); - - set_remove(wf->target->waiters, f); - sd_future_unref(wf->target); - free(wf); -} - -static int wait_future_cancel(sd_future *f) { - WaitFuture *wf = ASSERT_PTR(sd_future_get_private(ASSERT_PTR(f))); - - set_remove(wf->target->waiters, f); - return sd_future_resolve(f, -ECANCELED); -} - -static const sd_future_ops wait_future_ops = { - .size = sizeof(sd_future_ops), - .alloc = wait_future_alloc, - .free = wait_future_free, - .cancel = wait_future_cancel, -}; - -int sd_future_new_wait(sd_future *target, sd_future **ret) { - int r; - - assert_return(target, -EINVAL); - assert_return(ret, -EINVAL); - - _cleanup_(sd_future_unrefp) sd_future *f = NULL; - r = sd_future_new(&wait_future_ops, &f); - if (r < 0) - return r; - - WaitFuture *wf = sd_future_get_private(f); - wf->target = sd_future_ref(target); - - if (target->state == SD_FUTURE_RESOLVED) - r = sd_future_resolve(f, target->result); - else - r = set_ensure_put(&target->waiters, &trivial_hash_ops, f); - if (r < 0) - return r; - - *ret = TAKE_PTR(f); - return 0; -} diff --git a/src/libsystemd/sd-future/test-fiber-io.c b/src/libsystemd/sd-future/test-fiber-io.c index aef38950b6..2353324012 100644 --- a/src/libsystemd/sd-future/test-fiber-io.c +++ b/src/libsystemd/sd-future/test-fiber-io.c @@ -1111,6 +1111,48 @@ TEST(fiber_poll_timeout) { ASSERT_OK(sd_future_result(f)); } +typedef struct PollInterruptState { + int fd; + bool timeout; +} PollInterruptState; + +static int poll_interrupt_fiber(void *userdata) { + PollInterruptState *s = ASSERT_PTR(userdata); + struct pollfd pfd = { .fd = s->fd, .events = POLLIN }; + + { + SD_FIBER_TIMEOUT(s->timeout ? 0 : USEC_INFINITY); + ASSERT_EQ(sd_fiber_ppoll(&pfd, 1, /* timeout= */ NULL, /* sigmask= */ NULL), + s->timeout ? -ETIME : -ECANCELED); + ASSERT_EQ(pfd.revents, 0); + } + + ASSERT_OK_ZERO(sd_fiber_yield()); + return 0; +} + +TEST(fiber_poll_interrupted) { + bool timeout; + + FOREACH_ARGUMENT(timeout, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR; + + ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK)); + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + PollInterruptState s = { .fd = pipefd[0], .timeout = timeout }; + ASSERT_OK(sd_fiber_new(e, "poll-interrupt", poll_interrupt_fiber, &s, + /* destroy= */ NULL, &f)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + if (!timeout) + ASSERT_OK(sd_future_cancel(f)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(f)); + } +} + /* Test: poll with zero timeout (should not block) */ static int poll_zero_timeout_fiber(void *userdata) { int *pipefd = userdata; @@ -1192,6 +1234,30 @@ TEST(fiber_poll_zero_fds_no_timeout) { ASSERT_ERROR(sd_future_result(f), EINVAL); } +static int poll_all_entries_skipped_fiber(void *userdata) { + int *pipefd = ASSERT_PTR(userdata); + struct pollfd negative = { .fd = -EBADF, .events = POLLIN }; + struct pollfd no_events = { .fd = pipefd[0] }; + + ASSERT_ERROR(sd_fiber_ppoll(&negative, 1, /* timeout= */ NULL, /* sigmask= */ NULL), EINVAL); + ASSERT_ERROR(sd_fiber_ppoll(&no_events, 1, /* timeout= */ NULL, /* sigmask= */ NULL), EINVAL); + return 0; +} + +TEST(fiber_poll_all_entries_skipped) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR; + + ASSERT_OK_ERRNO(pipe2(pipefd, O_NONBLOCK | O_CLOEXEC)); + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_fiber_new(e, "poll-all-skipped", poll_all_entries_skipped_fiber, pipefd, + /* destroy= */ NULL, &f)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(f)); +} + /* Test: poll with negative fd (should be ignored) */ static int poll_negative_fd_fiber(void *userdata) { int *pipefd = userdata; diff --git a/src/libsystemd/sd-future/test-fiber.c b/src/libsystemd/sd-future/test-fiber.c index 2760b919a0..d9234892e6 100644 --- a/src/libsystemd/sd-future/test-fiber.c +++ b/src/libsystemd/sd-future/test-fiber.c @@ -649,6 +649,63 @@ TEST(fiber_nested_run_current_restored) { ASSERT_TRUE(slots[0] != slots[1]); } +static int cancel_running_ancestor_fiber(void *userdata) { + sd_future *ancestor = ASSERT_PTR(userdata); + + /* The ancestor is still executing on the call stack, but is not the current fiber. Cancelling + * it must queue an interruption, not mistake its first invocation for an unstarted fiber. */ + ASSERT_TRUE(ancestor != sd_fiber_get_current()); + ASSERT_OK_POSITIVE(sd_future_cancel(ancestor)); + ASSERT_EQ(sd_future_state(ancestor), SD_FUTURE_PENDING); + ASSERT_OK_ZERO(sd_future_cancel(ancestor)); + return 0; +} + +static int running_ancestor_fiber(void *userdata) { + bool *yield_first = ASSERT_PTR(userdata); + _cleanup_(sd_event_unrefp) sd_event *inner = NULL; + _cleanup_(sd_future_unrefp) sd_future *child = NULL; + + if (*yield_first) + ASSERT_OK_ZERO(sd_fiber_yield()); + + ASSERT_OK(sd_event_new(&inner)); + ASSERT_OK(sd_event_set_exit_on_idle(inner, true)); + ASSERT_OK(sd_fiber_new(inner, "cancel-ancestor", cancel_running_ancestor_fiber, + sd_fiber_get_current(), /* destroy= */ NULL, &child)); + + /* The ready child is dispatched inline by the nested loop, without suspending the ancestor. */ + ASSERT_OK(sd_event_loop(inner)); + ASSERT_OK_ZERO(sd_future_result(child)); + ASSERT_ERROR(sd_fiber_suspend(), ECANCELED); + + /* Consuming the queued cancellation must leave us running, with no dispatch queued. */ + return sd_fiber_suspend(); +} + +TEST(fiber_cancel_running_ancestor) { + bool yield_first; + + FOREACH_ARGUMENT(yield_first, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_fiber_new(e, "running-ancestor", running_ancestor_fiber, &yield_first, + /* destroy= */ NULL, &f)); + if (yield_first) + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + + ASSERT_EQ(sd_future_state(f), SD_FUTURE_PENDING); + ASSERT_OK_ZERO(sd_event_run(e, 0)); + ASSERT_OK(sd_fiber_resume(f, 42)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(sd_future_result(f), 42); + } +} + static int nested_cancellation_fiber(void *userdata) { int *counter = ASSERT_PTR(userdata); _cleanup_(sd_future_cancel_wait_unrefp) sd_future *nested = NULL; @@ -682,6 +739,77 @@ static int exit_loop_fiber(void *userdata) { return sd_event_exit(sd_fiber_get_event(), 0); } +static int exit_ready_fiber(void *userdata) { + ASSERT_ERROR(sd_fiber_yield(), ECANCELED); + *(bool*) ASSERT_PTR(userdata) = true; + return 0; +} + +TEST(fiber_exit_ready) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + bool finished = false; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_fiber_new(e, "exit-ready", exit_ready_fiber, &finished, /* destroy= */ NULL, &f)); + + /* Yield leaves the fiber READY without a queued result. The exit handler must deliver + * cancellation and run it in the same dispatch, without needing another iteration. */ + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_FALSE(finished); + ASSERT_OK(sd_event_exit(e, 0)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + + ASSERT_TRUE(finished); + ASSERT_EQ(sd_future_state(f), SD_FUTURE_RESOLVED); + ASSERT_OK_ZERO(sd_future_result(f)); + ASSERT_OK(sd_event_loop(e)); +} + +static int exit_ready_parent_fiber(void *userdata) { + sd_future **ret_child = ASSERT_PTR(userdata); + _cleanup_(sd_future_cancel_wait_unrefp) sd_future *child = NULL; + + ASSERT_OK(sd_fiber_new(sd_fiber_get_event(), "exit-ready-child", cancel_fiber, + /* userdata= */ NULL, /* destroy= */ NULL, &child)); + *ret_child = sd_future_ref(child); + return sd_fiber_suspend(); +} + +TEST(fiber_exit_ready_child) { + bool wake_during_exit; + int result; + + FOREACH_ARGUMENT(wake_during_exit, false, true) + FOREACH_ARGUMENT(result, 0, -ETIME, -ECANCELED) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *parent = NULL, *child = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_fiber_new(e, "exit-ready-parent", exit_ready_parent_fiber, &child, + /* destroy= */ NULL, &parent)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); /* Parent creates child and suspends. */ + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); /* Child yields, leaving only its defer armed. */ + + if (result == -ECANCELED) + ASSERT_OK_POSITIVE(sd_future_cancel(child)); + else if (result != 0) + ASSERT_OK(sd_fiber_resume(child, result)); + + ASSERT_OK(sd_event_exit(e, 0)); + if (wake_during_exit) + /* A sticky interruption must not prevent the pending dispatch from moving + * to the exit source, even between exit callbacks. */ + ASSERT_OK(sd_fiber_resume(child, 0)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(sd_future_state(child), SD_FUTURE_RESOLVED); + ASSERT_EQ(sd_future_state(parent), SD_FUTURE_RESOLVED); + ASSERT_ERROR(sd_future_result(child), ECANCELED); + ASSERT_ERROR(sd_future_result(parent), ECANCELED); + } +} + TEST(fiber_nested_cancellation) { _cleanup_(sd_event_unrefp) sd_event *e = NULL; ASSERT_OK(sd_event_new(&e)); @@ -817,7 +945,61 @@ TEST(fiber_floating) { ASSERT_EQ(counter, 2); } -static int drop_extra_ref(sd_future *f) { +static void fire_and_forget_destroy(void *userdata) { + int *counter = ASSERT_PTR(userdata); + + ASSERT_EQ(*counter, 2); + (*counter)++; +} + +TEST(fiber_fire_and_forget) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + int counter = 0; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_fiber_new(e, "fire-and-forget", floating_fiber, &counter, + fire_and_forget_destroy, /* ret= */ NULL)); + ASSERT_EQ(counter, 0); + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(counter, 3); +} + +static void unstarted_fiber_destroy(void *userdata) { + int *counter = ASSERT_PTR(userdata); + + ASSERT_EQ(*counter, 0); + (*counter)++; +} + +TEST(fiber_floating_exit_before_start) { + bool return_handle; + + FOREACH_ARGUMENT(return_handle, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + int counter = 0; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_fiber_new(e, "unstarted-floating", yielding_fiber, &counter, + unstarted_fiber_destroy, return_handle ? &f : NULL)); + if (f) { + ASSERT_OK(sd_fiber_set_floating(f, true)); + /* An already-queued cancellation must not bypass synchronous cancellation of + * a fiber whose stack has never been entered. */ + ASSERT_OK(sd_fiber_resume(f, -ECANCELED)); + f = sd_future_unref(f); + } + + /* Cancellation drops the floating self-reference without ever entering the fiber. + * The exit handler must return without accessing the freed fiber afterwards. */ + ASSERT_OK(sd_event_exit(e, 0)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(counter, 1); + } +} + +static int drop_extra_ref(sd_future *f, void *userdata) { /* Drop an extra ref the test installed before the callback fires. After this returns, the * floating self-ref is the only thing keeping the future alive — exercising the path where * the floating unref in fiber_run() is the last unref. */ @@ -836,9 +1018,10 @@ TEST(fiber_floating_callback_drops_ref) { ASSERT_OK(sd_fiber_set_floating(f, true)); - /* Bump the ref for the callback to drop, then install the callback. */ + /* Bump the ref for the callback to drop, then install the callback (floating slot, + * lifetime bound to f). */ sd_future_ref(f); - ASSERT_OK(sd_future_set_callback(f, drop_extra_ref, NULL)); + ASSERT_OK(sd_future_add_callback(f, NULL, drop_extra_ref, NULL)); /* Drop our handle. Refs remaining: floating self-ref + the extra ref the callback will drop. */ f = sd_future_unref(f); @@ -992,6 +1175,739 @@ TEST(fiber_timeout_nested) { ASSERT_EQ(fired, 2); } +/* Test: sd_future_cancel_wait_unref() loops on cancel + await until the future actually + * resolves, even if an outer SD_FIBER_TIMEOUT interrupts the await early. The stubborn + * future below requires multiple cancels to resolve, so a single cancel + interrupted await + * leaves it pending — only the loop in sd_future_cancel_wait_unref() can drive it to + * resolution. */ +typedef struct StubbornFuture { + unsigned cancels_received; + unsigned cancels_needed; + unsigned *external_counter; +} StubbornFuture; + +static void* stubborn_alloc(void) { + return new0(StubbornFuture, 1); +} + +static void stubborn_free(sd_future *f) { + free(sd_future_get_private(f)); +} + +static int stubborn_cancel(sd_future *f) { + StubbornFuture *sf = ASSERT_PTR(sd_future_get_private(f)); + sf->cancels_received++; + if (sf->external_counter) + *sf->external_counter = sf->cancels_received; + if (sf->cancels_received >= sf->cancels_needed) + return sd_future_resolve(f, -ECANCELED); + return 0; +} + +static const sd_future_ops stubborn_future_ops = { + .size = sizeof(sd_future_ops), + .alloc = stubborn_alloc, + .free = stubborn_free, + .cancel = stubborn_cancel, +}; + +static int cancel_wait_loops_fiber(void *userdata) { + unsigned *cancel_count = ASSERT_PTR(userdata); + sd_future *f = NULL; + int r; + + r = sd_future_new(sd_fiber_get_event(), &stubborn_future_ops, &f); + if (r < 0) + return r; + + StubbornFuture *sf = sd_future_get_private(f); + sf->cancels_needed = 2; + sf->external_counter = cancel_count; + + /* Short timeout interrupts the first await before the future resolves. The loop in + * sd_future_cancel_wait_unref() must call cancel a second time to drive the + * stubborn future to resolution. */ + SD_FIBER_TIMEOUT(5 * USEC_PER_MSEC); + sd_future_cancel_wait_unref(f); + return 0; +} + +TEST(fiber_cancel_wait_unref_loops_until_resolved) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + unsigned cancel_count = 0; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + ASSERT_OK(sd_fiber_new(e, "stubborn", cancel_wait_loops_fiber, &cancel_count, NULL, &f)); + + ASSERT_OK(sd_event_loop(e)); + /* Loop must have called cancel at least twice — once before the timeout interrupted + * the await, then again on the next iteration to actually resolve the future. */ + ASSERT_GE(cancel_count, 2u); +} + +/* Test: sd_fiber_resume() called on a running (not suspended) fiber queues the value rather than + * discarding it; the next fiber_swap() (here sd_fiber_suspend()) returns it without round-tripping + * through the event loop. If the swap actually yielded, this test would hang because nothing else + * is wired up to resume the fiber. */ +static int resume_queue_while_running_fiber(void *userdata) { + int r; + + r = sd_fiber_resume(sd_fiber_get_current(), 42); + if (r < 0) + return r; + + r = sd_fiber_suspend(); + if (r != 42) + return -EBADF; + + /* sd_fiber_yield() must also drain a queued value when it's set. */ + r = sd_fiber_resume(sd_fiber_get_current(), -EPIPE); + if (r < 0) + return r; + + r = sd_fiber_yield(); + if (r != -EPIPE) + return -EBADF; + + return 0; +} + +TEST(fiber_resume_queues_while_running) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + ASSERT_OK(sd_fiber_new(e, "resume-queue", resume_queue_while_running_fiber, NULL, NULL, &f)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK(sd_future_result(f)); +} + +static int self_resume_then_suspend_fiber(void *userdata) { + ASSERT_OK_ZERO(sd_fiber_yield()); /* Exercise self-resume after a real context switch, too. */ + ASSERT_OK(sd_fiber_resume(sd_fiber_get_current(), 42)); + ASSERT_EQ(sd_fiber_suspend(), 42); + ASSERT_OK(sd_fiber_resume(sd_fiber_get_current(), -EPIPE)); + ASSERT_ERROR(sd_fiber_yield(), EPIPE); + return sd_fiber_suspend(); +} + +TEST(fiber_self_resume_does_not_arm_source) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_fiber_new(e, "self-resume", self_resume_then_suspend_fiber, + /* userdata= */ NULL, /* destroy= */ NULL, &f)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); /* Yield. */ + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); /* Consume queued results, then really suspend. */ + + /* No source may dispatch until an external wakeup arrives. */ + ASSERT_OK_ZERO(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(f), SD_FUTURE_PENDING); + ASSERT_OK(sd_fiber_resume(f, 0)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(f)); +} + +static int self_resume_during_exit_fiber(void *userdata) { + ASSERT_ERROR(sd_fiber_yield(), ECANCELED); + ASSERT_EQ(sd_event_get_state(sd_fiber_get_event()), SD_EVENT_EXITING); + ASSERT_OK(sd_fiber_resume(sd_fiber_get_current(), 42)); + ASSERT_EQ(sd_fiber_suspend(), 42); + return sd_fiber_suspend(); +} + +static int resume_fiber_on_exit(sd_event_source *s, void *userdata) { + return sd_fiber_resume(userdata, 0); +} + +TEST(fiber_self_resume_during_exit_does_not_arm_source) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + _cleanup_(sd_event_source_unrefp) sd_event_source *resume = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_fiber_new(e, "self-resume-exit", self_resume_during_exit_fiber, + /* userdata= */ NULL, /* destroy= */ NULL, &f)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + + /* Cleanup must really suspend before this lower-priority source wakes it. A stray dispatch + * from self-resume would run the suspended fiber first and trip fiber_run()'s assertion. */ + ASSERT_OK(sd_event_add_exit(e, &resume, resume_fiber_on_exit, f)); + ASSERT_OK(sd_event_source_set_priority(resume, 1)); + ASSERT_OK(sd_event_exit(e, 0)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(f), ECANCELED); +} + +/* Test: a timeout that fires during sd_future_cancel_wait_unref()'s internal await must not be + * swallowed — cancel_wait_unref re-queues it via sd_fiber_resume() so the calling fiber's next + * suspend observes -ETIME. */ +static int cancel_wait_propagates_timeout_fiber(void *userdata) { + unsigned *cancel_count = ASSERT_PTR(userdata); + sd_future *f = NULL; + int r; + + r = sd_future_new(sd_fiber_get_event(), &stubborn_future_ops, &f); + if (r < 0) + return r; + + StubbornFuture *sf = sd_future_get_private(f); + sf->cancels_needed = 2; + sf->external_counter = cancel_count; + + SD_FIBER_TIMEOUT(5 * USEC_PER_MSEC); + sd_future_cancel_wait_unref(f); + + /* The timeout that interrupted the await inside cancel_wait_unref was re-queued; this + * suspend consumes it. */ + return sd_fiber_suspend(); +} + +TEST(fiber_cancel_wait_unref_propagates_timeout) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + unsigned cancel_count = 0; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + ASSERT_OK(sd_fiber_new(e, "propagate-timeout", cancel_wait_propagates_timeout_fiber, &cancel_count, NULL, &f)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(f), ETIME); + ASSERT_GE(cancel_count, 2u); +} + +/* Test: a cancellation of the calling fiber that lands while the fiber is suspended inside + * sd_future_cancel_wait_unref()'s internal await must not be swallowed — cancel_wait_unref + * re-queues it via sd_fiber_resume() so the next suspend on this fiber observes -ECANCELED. */ +static int cancel_wait_propagates_cancellation_fiber(void *userdata) { + unsigned *cancel_count = ASSERT_PTR(userdata); + sd_future *f = NULL; + int r; + + r = sd_future_new(sd_fiber_get_event(), &stubborn_future_ops, &f); + if (r < 0) + return r; + + StubbornFuture *sf = sd_future_get_private(f); + sf->cancels_needed = 2; + sf->external_counter = cancel_count; + + sd_future_cancel_wait_unref(f); + + /* If cancel_wait_unref had silently swallowed our cancellation, this suspend would + * actually park the fiber and the event loop would idle-exit without ever resolving the + * future — the assertion in the caller would then trip on a still-PENDING future. */ + return sd_fiber_suspend(); +} + +TEST(fiber_cancel_wait_unref_propagates_cancellation_from_main) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + unsigned cancel_count = 0; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + ASSERT_OK(sd_fiber_new(e, "propagate-cancel", cancel_wait_propagates_cancellation_fiber, &cancel_count, NULL, &f)); + + /* One iteration drives the fiber through its first cancel + await inside + * cancel_wait_unref, leaving it suspended waiting for the stubborn future. */ + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + + /* Cancel from outside any fiber. The fiber is still suspended inside cancel_wait_unref's + * await — this queues -ECANCELED on it and re-arms its defer source. */ + ASSERT_OK_POSITIVE(sd_future_cancel(f)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(f), ECANCELED); + ASSERT_GE(cancel_count, 2u); +} + +typedef struct { + sd_future *target; +} PeerCancellerData; + +static int peer_canceller_fiber(void *userdata) { + PeerCancellerData *data = ASSERT_PTR(userdata); + return sd_future_cancel(data->target); +} + +TEST(fiber_cancel_wait_unref_propagates_cancellation_from_peer_fiber) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + unsigned cancel_count = 0; + _cleanup_(sd_future_unrefp) sd_future *target = NULL, *canceller = NULL; + ASSERT_OK(sd_fiber_new(e, "target", cancel_wait_propagates_cancellation_fiber, &cancel_count, NULL, &target)); + ASSERT_OK(sd_future_set_priority(target, 0)); + + /* Lower-priority canceller runs after target has reached cancel_wait_unref's await and + * suspended; it cancels target from within a fiber dispatch. */ + PeerCancellerData data = { .target = target }; + ASSERT_OK(sd_fiber_new(e, "canceller", peer_canceller_fiber, &data, NULL, &canceller)); + ASSERT_OK(sd_future_set_priority(canceller, 1)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK(sd_future_result(canceller)); + ASSERT_ERROR(sd_future_result(target), ECANCELED); + ASSERT_GE(cancel_count, 2u); +} + +/* A future whose cancel resolves ASYNCHRONOUSLY (on a later event loop iteration) with + * -ECANCELED, rather than synchronously inside ops->cancel like StubbornFuture does. This + * verifies that cleanup treats the future's own negative result as completion, rather than + * re-queuing a phantom -ECANCELED onto the calling fiber. */ +typedef struct AsyncCancelFuture { + sd_event_source *resolve_source; +} AsyncCancelFuture; + +static void* async_cancel_alloc(void) { + return new0(AsyncCancelFuture, 1); +} + +static void async_cancel_free(sd_future *f) { + AsyncCancelFuture *af = sd_future_get_private(f); + sd_event_source_disable_unref(af->resolve_source); + free(af); +} + +static int async_cancel_resolve_handler(sd_event_source *s, void *userdata) { + return sd_future_resolve(ASSERT_PTR(userdata), -ECANCELED); +} + +static int async_cancel_cancel(sd_future *f) { + AsyncCancelFuture *af = ASSERT_PTR(sd_future_get_private(f)); + + if (af->resolve_source) + return 0; /* resolution already scheduled */ + + /* sd_event_add_defer leaves the source enabled ONESHOT, so it fires once on the next + * iteration and resolves the future then — making the cancel asynchronous. */ + return sd_event_add_defer(sd_future_get_event(f), &af->resolve_source, async_cancel_resolve_handler, f); +} + +static const sd_future_ops async_cancel_future_ops = { + .size = sizeof(sd_future_ops), + .alloc = async_cancel_alloc, + .free = async_cancel_free, + .cancel = async_cancel_cancel, +}; + +static int cancel_wait_async_resolve_fiber(void *userdata) { + sd_future *f = NULL; + int r; + + r = sd_future_new(sd_fiber_get_event(), &async_cancel_future_ops, &f); + if (r < 0) + return r; + + /* The future resolves asynchronously with -ECANCELED, but its completion wakes the internal + * wait with 0. No interruption targeted this fiber, so nothing must be re-queued. */ + sd_future_cancel_wait_unref(f); + + /* If a phantom -ECANCELED had been re-queued, this yield would return it instead of a + * clean 0 resume. */ + return sd_fiber_yield(); +} + +TEST(fiber_cancel_wait_unref_no_phantom_cancellation_on_async_resolve) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + ASSERT_OK(sd_fiber_new(e, "async-resolve", cancel_wait_async_resolve_fiber, NULL, NULL, &f)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK(sd_future_result(f)); +} + +static int cancel_wait_chain_fiber(void *userdata) { + unsigned mode = *(unsigned*) ASSERT_PTR(userdata); + + { + _cleanup_(sd_future_cancel_wait_unrefp) sd_future *timer = NULL; + if (mode > 0) { + ASSERT_NOT_NULL(timer = sd_fiber_timeout(0)); + /* In the combined case, let cancellation be consumed before the timer fires. */ + ASSERT_OK(sd_future_set_priority(timer, 100)); + } + _cleanup_(sd_future_cancel_wait_unrefp) sd_future *last = NULL, *middle = NULL, *first = NULL; + + ASSERT_OK(sd_future_new(sd_fiber_get_event(), &async_cancel_future_ops, &last)); + ASSERT_OK(sd_future_new(sd_fiber_get_event(), &async_cancel_future_ops, &middle)); + ASSERT_OK(sd_future_new(sd_fiber_get_event(), &stubborn_future_ops, &first)); + StubbornFuture *sf = sd_future_get_private(first); + sf->cancels_needed = mode == 2 ? 3 : 2; + } + + /* Each cleanup must forward the interruption to the next, even when the next await consumes + * it without yielding. A later timeout must not replace an earlier cancellation. */ + ASSERT_EQ(sd_fiber_yield(), mode == 1 ? -ETIME : -ECANCELED); + ASSERT_OK_ZERO(sd_fiber_yield()); + return 0; +} + +TEST(fiber_cancel_wait_unref_chain) { + for (unsigned mode = 0; mode < 3; mode++) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_fiber_new(e, "cleanup-chain", cancel_wait_chain_fiber, &mode, + /* destroy= */ NULL, &f)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + if (mode != 1) + ASSERT_OK(sd_future_cancel(f)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(f)); + } +} + +/* A future whose cancel is a no-op: it never resolves on its own, only when the test resolves it + * explicitly. This lets the test stage an exact collision between an external cancellation of the + * calling fiber and the awaited future resolving in the same event loop iteration — the case where + * sd_fiber_await() returns with the future already RESOLVED yet the negative value came from the + * cancellation, not the future. The future is resolved with 0 (success) so that its own completion + * notification can't masquerade as the cancellation: only a correctly forwarded interruption makes + * the calling fiber observe -ECANCELED after cleanup. */ +typedef struct ManualFuture { + bool *freed; +} ManualFuture; + +static void* manual_alloc(void) { + return new0(ManualFuture, 1); +} + +static void manual_free(sd_future *f) { + ManualFuture *mf = ASSERT_PTR(sd_future_get_private(f)); + + if (mf->freed) + *mf->freed = true; + free(mf); +} + +static int manual_cancel(sd_future *f) { + return 0; /* stays PENDING; the test drives resolution explicitly */ +} + +static const sd_future_ops manual_future_ops = { + .size = sizeof(sd_future_ops), + .alloc = manual_alloc, + .free = manual_free, + .cancel = manual_cancel, +}; + +TEST(future_new_default_event) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + + ASSERT_OK_ZERO(sd_event_default(/* ret= */ NULL)); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_new(/* e= */ NULL, &manual_future_ops, &f)), EINVAL); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_new(SD_EVENT_DEFAULT, &manual_future_ops, &f)), ENOPKG); + ASSERT_NULL(f); + ASSERT_OK_ZERO(sd_event_default(/* ret= */ NULL)); + + ASSERT_OK(sd_event_default(&e)); + ASSERT_OK(sd_future_new(SD_EVENT_DEFAULT, &manual_future_ops, &f)); + ASSERT_PTR_EQ(sd_future_get_event(f), e); + + /* The future must own a reference to the actual loop, not retain the default-event sentinel. */ + e = sd_event_unref(e); + ASSERT_OK_POSITIVE(sd_event_default(/* ret= */ NULL)); + ASSERT_OK(sd_future_resolve(f, 42)); + f = sd_future_unref(f); + ASSERT_OK_ZERO(sd_event_default(/* ret= */ NULL)); +} + +static int default_event_fiber(void *userdata) { + sd_event *e = ASSERT_PTR(userdata); + _cleanup_(sd_future_cancel_wait_unrefp) sd_future *group = NULL, *child = NULL; + + ASSERT_PTR_EQ(sd_fiber_get_event(), e); + ASSERT_OK(sd_future_group_new(SD_EVENT_DEFAULT, &group)); + ASSERT_OK(sd_future_new_defer(SD_EVENT_DEFAULT, -EIO, &child)); + ASSERT_PTR_EQ(sd_future_get_event(group), e); + ASSERT_PTR_EQ(sd_future_get_event(child), e); + ASSERT_OK(sd_future_group_add(group, child)); + + /* Using the sentinel must not prevent the group from capturing and cancelling its parent. */ + ASSERT_ERROR(sd_fiber_suspend(), ECANCELED); + ASSERT_ERROR(sd_future_result(group), EIO); + return 0; +} + +TEST(future_default_event_constructors) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + + ASSERT_OK(sd_event_default(&e)); + ASSERT_OK(sd_fiber_new(SD_EVENT_DEFAULT, "default-event", default_event_fiber, e, + /* destroy= */ NULL, &f)); + while (ASSERT_OK(sd_event_run(e, 0)) > 0) + ; + + /* No exit-on-idle: the child error, not loop shutdown, must have resumed the parent. */ + ASSERT_EQ(sd_future_state(f), SD_FUTURE_RESOLVED); + ASSERT_OK_ZERO(sd_future_result(f)); +} + +static int await_result_fiber(void *userdata) { + int result; + + FOREACH_ARGUMENT(result, 0, 42, -ECANCELED, -ETIME, -EIO) { + _cleanup_(sd_future_unrefp) sd_future *awaited = NULL; + + ASSERT_OK(sd_future_new_defer(sd_fiber_get_event(), result, &awaited)); + ASSERT_EQ(sd_fiber_await(awaited), result); /* Pending. */ + ASSERT_EQ(sd_fiber_await(awaited), result); /* Already resolved. */ + ASSERT_OK_ZERO(sd_fiber_yield()); + + /* An already-resolved future must leave an unrelated pending interruption alone. */ + ASSERT_OK(sd_fiber_resume(sd_fiber_get_current(), -ECANCELED)); + ASSERT_EQ(sd_fiber_await(awaited), result); + ASSERT_ERROR(sd_fiber_yield(), ECANCELED); + } + + return 0; +} + +TEST(fiber_await_result) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_fiber_new(e, "await-result", await_result_fiber, /* userdata= */ NULL, + /* destroy= */ NULL, &f)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(f)); +} + +static int await_borrowed_fiber(void *userdata) { + return sd_fiber_await(userdata); +} + +TEST(fiber_await_last_reference) { + int result; + + FOREACH_ARGUMENT(result, 0, 42, -ECANCELED, -ETIME, -EIO) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *target = NULL, *waiter = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_future_new(e, &manual_future_ops, &target)); + ASSERT_OK(sd_fiber_new(e, "await-borrowed", await_borrowed_fiber, target, + /* destroy= */ NULL, &waiter)); + + /* Suspend in await, so its non-floating callback slot holds a reference to target. */ + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(waiter), SD_FUTURE_PENDING); + + /* Leave the slot as the sole owner. Await must read the result before freeing it. */ + ASSERT_OK(sd_future_resolve(target, result)); + target = sd_future_unref(target); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(sd_future_result(waiter), result); + } +} + +typedef struct AwaitInvalidState { + sd_future *other; + bool freed; +} AwaitInvalidState; + +static void await_invalid_destroy(void *userdata) { + AwaitInvalidState *s = ASSERT_PTR(userdata); + + s->freed = true; +} + +static int await_invalid_fiber(void *userdata) { + AwaitInvalidState *s = ASSERT_PTR(userdata); + sd_future *other = s->other; + _cleanup_(sd_future_unrefp) sd_future *synchronous = NULL; + + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_fiber_await(NULL)), EINVAL); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_fiber_await(sd_fiber_get_current())), EDEADLK); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_fiber_await(other)), EINVAL); + ASSERT_NULL(ASSERT_RETURN_EXPECTED(sd_future_cancel_wait_unref(sd_future_ref(sd_fiber_get_current())))); + ASSERT_NULL(ASSERT_RETURN_EXPECTED(sd_future_cancel_wait_unref(sd_future_ref(other)))); + ASSERT_EQ(sd_future_state(other), SD_FUTURE_PENDING); + + ASSERT_OK(sd_future_resolve(other, 42)); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_fiber_await(other)), EINVAL); + + /* Cleanup only needs the same loop if cancellation actually has to wait. */ + ASSERT_NULL(sd_future_cancel_wait_unref(sd_future_ref(other))); + ASSERT_OK(sd_future_new_defer(sd_future_get_event(other), 0, &synchronous)); + ASSERT_NULL(sd_future_cancel_wait_unref(TAKE_PTR(synchronous))); + return 0; +} + +TEST(fiber_await_invalid) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL, *other = NULL; + _cleanup_(sd_future_unrefp) sd_future *target = NULL, *waiter = NULL; + AwaitInvalidState s = {}; + bool target_freed = false; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_new(&other)); + ASSERT_OK(sd_future_new(other, &manual_future_ops, &target)); + ManualFuture *mf = sd_future_get_private(target); + mf->freed = &target_freed; + s.other = target; + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_fiber_await(target)), ESRCH); + ASSERT_OK(sd_fiber_new(e, "await-invalid", await_invalid_fiber, &s, await_invalid_destroy, &waiter)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(waiter)); + ASSERT_EQ(sd_future_result(target), 42); + ASSERT_OK_ZERO(sd_event_run(other, 0)); + + /* Invalid cleanup calls consume their owned references too, not just valid calls. */ + waiter = sd_future_unref(waiter); + ASSERT_TRUE(s.freed); + target = sd_future_unref(target); + ASSERT_TRUE(target_freed); +} + +TEST(fiber_get_awaiting) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *target = NULL, *waiter = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_new(e, &manual_future_ops, &target)); + ASSERT_NULL(ASSERT_RETURN_EXPECTED(sd_fiber_get_awaiting(target))); + ASSERT_OK(sd_fiber_new(e, "get-awaiting", await_borrowed_fiber, target, /* destroy= */ NULL, &waiter)); + ASSERT_NULL(sd_fiber_get_awaiting(waiter)); + + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_PTR_EQ(sd_fiber_get_awaiting(waiter), target); + + /* Resolution only queues the wake-up; the fiber keeps awaiting until it actually runs. */ + ASSERT_OK(sd_future_resolve(target, 42)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_PTR_EQ(sd_fiber_get_awaiting(waiter), target); + + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_NULL(sd_fiber_get_awaiting(waiter)); + ASSERT_EQ(sd_future_result(waiter), 42); +} + +TEST(fiber_await_resumed_before_completion) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *target = NULL, *waiter = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_new(e, &manual_future_ops, &target)); + ASSERT_OK(sd_fiber_new(e, "await-resumed", await_borrowed_fiber, target, /* destroy= */ NULL, &waiter)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(waiter), SD_FUTURE_PENDING); + + /* An unrelated successful resume is not completion of the target. The abandoned wait must + * unregister its callback, so resolving the target later cannot resume the finished fiber. */ + ASSERT_OK(sd_fiber_resume(waiter, 42)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_ERROR(sd_future_result(waiter), EBUSY); + ASSERT_EQ(sd_future_state(target), SD_FUTURE_PENDING); + ASSERT_OK(sd_future_resolve(target, 0)); + ASSERT_OK_ZERO(sd_event_run(e, 0)); +} + +static int cancel_wait_collision_fiber(void *userdata) { + sd_future *mf = ASSERT_PTR(userdata); + + /* Take our own ref; cancel_wait_unref consumes one while the test keeps the original to + * resolve mf and for final cleanup. */ + sd_future_cancel_wait_unref(sd_future_ref(mf)); + + /* A genuine external cancellation coincided with mf resolving (with success). It must have + * been re-queued, so this yield observes -ECANCELED rather than a clean 0. We use yield rather + * than suspend deliberately: yield always reschedules promptly and completes before the loop + * goes idle, so a *missing* re-queue surfaces as a clean 0 here — whereas a parked suspend + * would instead be rescued by the event loop's exit-on-idle cancellation, masking the bug. */ + return sd_fiber_yield(); +} + +TEST(fiber_cancel_wait_unref_propagates_cancellation_colliding_with_resolve) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *mf = NULL; + ASSERT_OK(sd_future_new(e, &manual_future_ops, &mf)); + + _cleanup_(sd_future_unrefp) sd_future *fib = NULL; + ASSERT_OK(sd_fiber_new(e, "collision", cancel_wait_collision_fiber, mf, NULL, &fib)); + + /* Drive the fiber to suspend inside cancel_wait_unref's await on mf (mf's no-op cancel leaves + * it PENDING). */ + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + + /* In one shot — with no loop dispatch in between — resolve mf successfully *and* cancel the + * calling fiber. Both arm a resume for the next iteration, so the await returns -ECANCELED with + * mf already RESOLVED: the exact collision. The genuine fiber cancellation must survive, and + * mf's own 0 result must not be what the fiber ends up observing. */ + ASSERT_OK(sd_future_resolve(mf, 0)); + ASSERT_OK_POSITIVE(sd_future_cancel(fib)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(fib), ECANCELED); +} + +/* Test: an interruption seen in one wait iteration must not be clobbered by the awaited future + * resolving with its own negative result in a later iteration. cancel_wait_unref remembers + * interruptions separately from the future's result, so mf's late -EIO resolution must not overwrite + * the earlier -ECANCELED interruption. */ +static int cancel_wait_keeps_interrupt_value_fiber(void *userdata) { + sd_future *mf = ASSERT_PTR(userdata); + + sd_future_cancel_wait_unref(sd_future_ref(mf)); + + /* Must observe the earlier -ECANCELED interruption, not mf's -EIO resolution. Yield (not + * suspend) so a missing re-queue surfaces as a clean 0 rather than being masked by exit-on-idle + * cancellation. */ + return sd_fiber_yield(); +} + +TEST(fiber_cancel_wait_unref_keeps_interrupt_value_over_later_resolution) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *mf = NULL; + ASSERT_OK(sd_future_new(e, &manual_future_ops, &mf)); + + _cleanup_(sd_future_unrefp) sd_future *fib = NULL; + ASSERT_OK(sd_fiber_new(e, "keep-interrupt", cancel_wait_keeps_interrupt_value_fiber, mf, NULL, &fib)); + + /* Reach the first await on mf (no-op cancel leaves mf PENDING). */ + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + + /* Deliver an interruption while mf is still pending: cancel_wait_unref remembers -ECANCELED and + * loops back to await mf again (now suspended in the second iteration). */ + ASSERT_OK_POSITIVE(sd_future_cancel(fib)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + + /* Resolve mf with its own negative result. This is a completion, so it must + * not overwrite the remembered -ECANCELED — with the bug, the fiber would observe -EIO. */ + ASSERT_OK(sd_future_resolve(mf, -EIO)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(fib), ECANCELED); +} + /* Test: signal mask is per-thread, not per-fiber. Changes one fiber makes via pthread_sigmask * must be visible to other fibers on the same thread, both while the modifying fiber is * suspended and after it resumes. The fiber switch (sigsetjmp/siglongjmp with savesigs=0) @@ -1168,4 +2084,542 @@ TEST(fiber_stack_guard) { ASSERT_TRUE(IN_SET(si.si_status, SIGSEGV, SIGBUS)); } +static int counting_callback(sd_future *f, void *userdata) { + int *counter = ASSERT_PTR(userdata); + (*counter)++; + return 0; +} + +typedef struct SlotPriorityState { + sd_future *target; + sd_future_slot *slot; + unsigned count; + bool floating; +} SlotPriorityState; + +static int slot_priority_callback(sd_future *f, void *userdata) { + SlotPriorityState *s = ASSERT_PTR(userdata); + + ASSERT_EQ(sd_future_result(f), 42); + ASSERT_EQ(s->count++, 0U); + return 0; +} + +static int slot_priority_probe(sd_event_source *source, void *userdata) { + SlotPriorityState *s = ASSERT_PTR(userdata); + + ASSERT_EQ(s->count++, 1U); + return 0; +} + +static int slot_priority_register(void *userdata) { + SlotPriorityState *s = ASSERT_PTR(userdata); + + return sd_future_add_callback(s->target, s->floating ? NULL : &s->slot, slot_priority_callback, s); +} + +TEST(future_slot_inherits_fiber_priority) { + bool floating, exiting; + + FOREACH_ARGUMENT(floating, false, true) + FOREACH_ARGUMENT(exiting, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_event_source_unrefp) sd_event_source *probe = NULL; + _cleanup_(sd_future_unrefp) sd_future *target = NULL, *registrar = NULL; + SlotPriorityState s = { .floating = floating }; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_new(e, &manual_future_ops, &target)); + s.target = target; + + if (exiting) + ASSERT_OK(sd_event_add_exit(e, &probe, slot_priority_probe, &s)); + else + ASSERT_OK(sd_event_add_defer(e, &probe, slot_priority_probe, &s)); + + ASSERT_OK(sd_fiber_new(e, "register-slot", slot_priority_register, &s, + /* destroy= */ NULL, ®istrar)); + ASSERT_OK(sd_future_set_priority(registrar, -10)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_OK_ZERO(sd_future_result(registrar)); + ASSERT_EQ(s.count, 0U); + + /* Both slot sources must inherit the registrar's priority: its callback must + * overtake the older normal-priority probe during regular dispatch and exit. */ + ASSERT_OK(sd_future_resolve(target, 42)); + if (exiting) + ASSERT_OK(sd_event_exit(e, 0)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(s.count, 2U); + sd_future_slot_unref(s.slot); + } +} + +typedef struct ExitSlotState { + sd_future *target; + sd_future_slot *slot; + int count; + bool floating; + bool resolved; +} ExitSlotState; + +static int exit_slot_callback(sd_future *f, void *userdata) { + ExitSlotState *s = ASSERT_PTR(userdata); + + ASSERT_EQ(sd_event_get_state(sd_future_get_event(f)), SD_EVENT_EXITING); + ASSERT_EQ(sd_future_result(f), 42); + s->count++; + return 0; +} + +static int exit_slot_setup(sd_event_source *source, void *userdata) { + ExitSlotState *s = ASSERT_PTR(userdata); + + ASSERT_EQ(sd_event_get_state(sd_event_source_get_event(source)), SD_EVENT_EXITING); + if (s->resolved) + ASSERT_OK(sd_future_resolve(s->target, 42)); + + ASSERT_OK(sd_future_add_callback(s->target, s->floating ? NULL : &s->slot, exit_slot_callback, s)); + if (!s->resolved) + ASSERT_OK(sd_future_resolve(s->target, 42)); + + ASSERT_EQ(s->count, 0); + return 0; +} + +TEST(future_slot_exit) { + bool floating, resolved; + + FOREACH_ARGUMENT(floating, false, true) + FOREACH_ARGUMENT(resolved, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_event_source_unrefp) sd_event_source *exit_source = NULL; + _cleanup_(sd_future_unrefp) sd_future *target = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_new(e, &manual_future_ops, &target)); + + ExitSlotState s = { .target = target, .floating = floating, .resolved = resolved }; + ASSERT_OK(sd_event_add_exit(e, &exit_source, exit_slot_setup, &s)); + ASSERT_OK(sd_event_exit(e, 0)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(s.count, 1); + sd_future_slot_unref(s.slot); + } +} + +TEST(future_slot_exit_transition) { + bool floating, resolved; + unsigned phase; + + FOREACH_ARGUMENT(floating, false, true) + FOREACH_ARGUMENT(resolved, false, true) + FOREACH_ARGUMENT(phase, 0u, 1u, 2u) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + _cleanup_(sd_future_slot_unrefp) sd_future_slot *slot = NULL; + int count = 0; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_new(e, &manual_future_ops, &f)); + + /* Request exit before queueing the callback, after queueing it, or after + * dispatching it normally. It must run exactly once in every case. */ + if (phase == 0) + ASSERT_OK(sd_event_exit(e, 0)); + + if (resolved) + ASSERT_OK(sd_future_resolve(f, 42)); + ASSERT_OK(sd_future_add_callback(f, floating ? NULL : &slot, counting_callback, &count)); + if (!resolved) + ASSERT_OK(sd_future_resolve(f, 42)); + ASSERT_EQ(count, 0); + + if (phase == 2) { + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_EQ(count, 1); + } + if (phase != 0) + ASSERT_OK(sd_event_exit(e, 0)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(count, 1); + } +} + +TEST(future_resolve_twice) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + _cleanup_(sd_future_slot_unrefp) sd_future_slot *slot = NULL; + int count = 0; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_new(e, &manual_future_ops, &f)); + ASSERT_OK(sd_future_add_callback(f, &slot, counting_callback, &count)); + ASSERT_OK(sd_future_resolve(f, 42)); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_resolve(f, 0)), ESTALE); + ASSERT_EQ(sd_future_result(f), 42); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_EQ(count, 1); + + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_resolve(f, -EINVAL)), ESTALE); + ASSERT_EQ(sd_future_result(f), 42); + ASSERT_OK_ZERO(sd_event_run(e, 0)); + ASSERT_EQ(count, 1); +} + +TEST(future_resolve_after_event_finished) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + _cleanup_(sd_future_slot_unrefp) sd_future_slot *slot = NULL; + int count = 0; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_new(e, &manual_future_ops, &f)); + ASSERT_OK(sd_future_add_callback(f, &slot, counting_callback, &count)); + ASSERT_OK(sd_event_exit(e, 0)); + ASSERT_OK(sd_event_loop(e)); + + /* Resolution is final even when its notification cannot be scheduled on a finished loop. */ + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_resolve(f, 42)), ESTALE); + ASSERT_EQ(sd_future_result(f), 42); + ASSERT_EQ(count, 0); +} + +TEST(future_cancel_wait_unref_without_fiber) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_new_defer(e, 0, &f)); + ASSERT_NULL(sd_future_cancel_wait_unref(sd_future_ref(f))); + ASSERT_ERROR(sd_future_result(f), ECANCELED); + ASSERT_NULL(sd_future_cancel_wait_unref(TAKE_PTR(f))); + ASSERT_NULL(sd_future_cancel_wait_unref(NULL)); +} + +/* Two callbacks on the same future both fire on resolution; a third whose slot is + * dropped before resolution never fires. */ +TEST(future_slot_lifecycle) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *target = NULL; + ASSERT_OK(sd_future_new_defer(e, 0, &target)); + + _cleanup_(sd_future_slot_unrefp) sd_future_slot *slot_a = NULL; + _cleanup_(sd_future_slot_unrefp) sd_future_slot *slot_b = NULL; + sd_future_slot *slot_c = NULL; + int a = 0, b = 0, c = 0; + + ASSERT_OK(sd_future_add_callback(target, &slot_a, counting_callback, &a)); + ASSERT_OK(sd_future_add_callback(target, &slot_b, counting_callback, &b)); + ASSERT_OK(sd_future_add_callback(target, &slot_c, counting_callback, &c)); + sd_future_slot_unref(slot_c); + + ASSERT_OK(sd_event_loop(e)); + + ASSERT_EQ(a, 1); + ASSERT_EQ(b, 1); + ASSERT_EQ(c, 0); +} + +TEST(future_floating_slot_fires_on_resolve) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *target = NULL; + ASSERT_OK(sd_future_new_defer(e, 0, &target)); + + int count = 0; + ASSERT_OK(sd_future_add_callback(target, NULL, counting_callback, &count)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(count, 1); +} + +static int defer_basic_fiber(void *userdata) { + int *counter = ASSERT_PTR(userdata); + _cleanup_(sd_future_unrefp) sd_future *defer = NULL; + _cleanup_(sd_future_slot_unrefp) sd_future_slot *slot = NULL; + + ASSERT_OK(sd_future_new_defer(sd_fiber_get_event(), 0, &defer)); + ASSERT_OK(sd_future_add_callback(defer, &slot, counting_callback, counter)); + return sd_fiber_await(defer); +} + +TEST(future_new_defer_basic) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + int count = 0; + _cleanup_(sd_future_unrefp) sd_future *driver = NULL; + ASSERT_OK(sd_fiber_new(e, "driver", defer_basic_fiber, &count, NULL, &driver)); + + ASSERT_OK(sd_event_loop(e)); + + ASSERT_EQ(count, 1); + ASSERT_OK_ZERO(sd_future_result(driver)); +} + +/* sd_future_add_callback on a RESOLVED future defers the callback to the next event-loop + * iteration; never fires inline. */ +TEST(add_callback_resolved_no_fiber_defers) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + + _cleanup_(sd_future_unrefp) sd_future *target = NULL; + ASSERT_OK(sd_future_new_defer(e, 0, &target)); + + /* Drive the loop manually — sd_event_loop would transition to FINISHED and block + * the subsequent sd_event_add_defer. */ + do + ASSERT_OK(sd_event_run(e, 0)); + while (sd_future_state(target) == SD_FUTURE_PENDING); + + int count = 0; + _cleanup_(sd_future_slot_unrefp) sd_future_slot *slot = NULL; + ASSERT_OK(sd_future_add_callback(target, &slot, counting_callback, &count)); + ASSERT_EQ(count, 0); + + ASSERT_OK(sd_event_run(e, 0)); + ASSERT_EQ(count, 1); +} + +/* Same as above with a floating slot — also defers. */ +TEST(add_callback_resolved_no_fiber_floating_defers) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + + _cleanup_(sd_future_unrefp) sd_future *target = NULL; + ASSERT_OK(sd_future_new_defer(e, 0, &target)); + + do + ASSERT_OK(sd_event_run(e, 0)); + while (sd_future_state(target) == SD_FUTURE_PENDING); + + int count = 0; + ASSERT_OK(sd_future_add_callback(target, NULL, counting_callback, &count)); + ASSERT_EQ(count, 0); + + ASSERT_OK(sd_event_run(e, 0)); + ASSERT_EQ(count, 1); +} + +typedef struct FloatingFreedState { + sd_future *target; + int fired_count; +} FloatingFreedState; + +static int floating_freed_driver(void *userdata) { + FloatingFreedState *s = ASSERT_PTR(userdata); + + ASSERT_OK(sd_fiber_await(s->target)); + ASSERT_EQ(sd_future_state(s->target), SD_FUTURE_RESOLVED); + + ASSERT_OK(sd_future_add_callback(s->target, NULL, counting_callback, &s->fired_count)); + + /* Drop the last external ref before the defer tick. The future owns the floating + * slot; freeing it must tear the slot's defer source down rather than leave it + * firing with a stale userdata. */ + s->target = sd_future_unref(s->target); + + ASSERT_OK(sd_fiber_yield()); + return 0; +} + +TEST(add_callback_resolved_in_fiber_floating_future_freed) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + FloatingFreedState s = {}; + ASSERT_OK(sd_future_new_defer(e, 0, &s.target)); + + _cleanup_(sd_future_unrefp) sd_future *driver = NULL; + ASSERT_OK(sd_fiber_new(e, "driver", floating_freed_driver, &s, NULL, &driver)); + + ASSERT_OK(sd_event_loop(e)); + + ASSERT_EQ(s.fired_count, 0); +} + +typedef struct DeferCancelState { + sd_future *target; + int fired_count; +} DeferCancelState; + +static int defer_cancel_driver(void *userdata) { + DeferCancelState *s = ASSERT_PTR(userdata); + sd_future_slot *slot = NULL; + + (void) sd_fiber_await(s->target); + ASSERT_EQ(sd_future_state(s->target), SD_FUTURE_RESOLVED); + + ASSERT_OK(sd_future_add_callback(s->target, &slot, counting_callback, &s->fired_count)); + + /* Drop the slot before the defer fires; the wrapping slot owns the defer source. */ + sd_future_slot_unref(slot); + + ASSERT_OK(sd_fiber_yield()); + return 0; +} + +TEST(defer_slot_cancel_before_fire) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *target = NULL; + ASSERT_OK(sd_future_new_defer(e, 0, &target)); + + DeferCancelState s = { .target = target }; + _cleanup_(sd_future_unrefp) sd_future *driver = NULL; + ASSERT_OK(sd_fiber_new(e, "driver", defer_cancel_driver, &s, NULL, &driver)); + + ASSERT_OK(sd_event_loop(e)); + + ASSERT_EQ(s.fired_count, 0); +} + +/* Test: once -ECANCELED is queued on a fiber, a concurrent async wakeup with a different value + * (e.g. a timer firing, an io_uring CQE result) must not overwrite it. The fiber observes + * -ECANCELED on its next suspend, and the override value is dropped. */ +static int sticky_cancel_fiber(void *userdata) { + int r; + + /* Queue -ECANCELED on ourselves while RUNNING. */ + r = sd_fiber_resume(sd_fiber_get_current(), -ECANCELED); + if (r < 0) + return r; + + /* Try to override with a different value — must be a no-op. The return value of + * sd_fiber_resume is 0 either way; what we care about is the value actually + * observed by the next yield. */ + r = sd_fiber_resume(sd_fiber_get_current(), -EPIPE); + if (r < 0) + return r; + + /* Likewise: a positive override is also dropped. */ + r = sd_fiber_resume(sd_fiber_get_current(), 42); + if (r < 0) + return r; + + /* Next suspend consumes the stashed value: must be -ECANCELED, not -EPIPE or 42. */ + return sd_fiber_yield(); +} + +TEST(fiber_resume_cancellation_is_sticky) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + ASSERT_OK(sd_fiber_new(e, "sticky", sticky_cancel_fiber, NULL, NULL, &f)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(f), ECANCELED); +} + +/* Test: -ETIME (an SD_FIBER_TIMEOUT firing) is sticky just like -ECANCELED — a concurrent normal + * completion (negative error or positive payload) must not overwrite a pending timeout. */ +static int sticky_timeout_fiber(void *userdata) { + int r; + + /* Queue -ETIME on ourselves while RUNNING. */ + r = sd_fiber_resume(sd_fiber_get_current(), -ETIME); + if (r < 0) + return r; + + /* Normal completions must not override the pending timeout. */ + r = sd_fiber_resume(sd_fiber_get_current(), -EPIPE); + if (r < 0) + return r; + + r = sd_fiber_resume(sd_fiber_get_current(), 42); + if (r < 0) + return r; + + /* Next suspend consumes the stashed value: must be -ETIME, not -EPIPE or 42. */ + return sd_fiber_yield(); +} + +TEST(fiber_resume_timeout_is_sticky) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *f = NULL; + ASSERT_OK(sd_fiber_new(e, "sticky-timeout", sticky_timeout_fiber, NULL, NULL, &f)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(f), ETIME); +} + +/* Test: -ECANCELED outranks -ETIME. A cancellation may escalate a pending timeout, but a timeout + * may not downgrade a pending cancellation. */ +static int timeout_then_cancel_fiber(void *userdata) { + int r; + + r = sd_fiber_resume(sd_fiber_get_current(), -ETIME); + if (r < 0) + return r; + + /* Cancellation escalates the pending timeout. */ + r = sd_fiber_resume(sd_fiber_get_current(), -ECANCELED); + if (r < 0) + return r; + + return sd_fiber_yield(); /* must observe -ECANCELED */ +} + +static int cancel_then_timeout_fiber(void *userdata) { + int r; + + r = sd_fiber_resume(sd_fiber_get_current(), -ECANCELED); + if (r < 0) + return r; + + /* Timeout must not downgrade the pending cancellation. */ + r = sd_fiber_resume(sd_fiber_get_current(), -ETIME); + if (r < 0) + return r; + + return sd_fiber_yield(); /* must still observe -ECANCELED */ +} + +TEST(fiber_resume_cancel_outranks_timeout) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *escalate = NULL, *keep = NULL; + ASSERT_OK(sd_fiber_new(e, "escalate", timeout_then_cancel_fiber, NULL, NULL, &escalate)); + ASSERT_OK(sd_fiber_new(e, "keep", cancel_then_timeout_fiber, NULL, NULL, &keep)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(escalate), ECANCELED); + ASSERT_ERROR(sd_future_result(keep), ECANCELED); +} + +/* Test: sd_future_new_defer() refuses to create a future when the event loop has already + * entered the EXITING/FINISHED phase — there's no future event-loop iteration left to fire + * the defer source on. Returns -ECANCELED. */ +TEST(future_new_defer_rejected_on_exiting_loop) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + + /* Drive the loop to FINISHED by asking it to exit immediately. */ + ASSERT_OK(sd_event_exit(e, 0)); + ASSERT_OK(sd_event_loop(e)); + + sd_future *f = NULL; + ASSERT_ERROR(sd_future_new_defer(e, 0, &f), ECANCELED); + ASSERT_NULL(f); +} + DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/libsystemd/sd-future/test-future-group.c b/src/libsystemd/sd-future/test-future-group.c new file mode 100644 index 0000000000..b33f83348a --- /dev/null +++ b/src/libsystemd/sd-future/test-future-group.c @@ -0,0 +1,1156 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-event.h" +#include "sd-future.h" + +#include "alloc-util.h" +#include "tests.h" + +/* Body for "I exist to be cancelled" siblings: suspends until something cancels us. */ +static int suspend_fiber(void *userdata) { + return sd_fiber_suspend(); +} + +/* Body for "let other things make progress before I return": yields once (giving the event + * loop a chance to dispatch other pending sources) then returns the configured result. + * Useful for sequencing without a real timer — sd-event dispatches at the same priority by + * pending iteration, so a rearmed source goes to the back of the queue. */ +static int yield_then_return_fiber(void *userdata) { + int *result = ASSERT_PTR(userdata); + int r = sd_fiber_yield(); + if (r < 0) + return r; + return *result; +} + +TEST(future_group_empty) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + uint64_t policy; + + ASSERT_OK(sd_event_new(&e)); + + FOREACH_ARGUMENT(policy, SD_FUTURE_GROUP_WAIT_ALL, SD_FUTURE_GROUP_WAIT_ANY, + SD_FUTURE_GROUP_IGNORE_ERRORS, + SD_FUTURE_GROUP_WAIT_ANY | SD_FUTURE_GROUP_IGNORE_ERRORS) { + _cleanup_(sd_future_unrefp) sd_future *group = NULL; + + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_group_set_policy(group, policy)); + ASSERT_OK_ZERO(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(group), SD_FUTURE_PENDING); + ASSERT_EQ(sd_future_group_size(group), 0U); + + ASSERT_OK(sd_future_cancel(group)); + ASSERT_EQ(sd_future_state(group), SD_FUTURE_RESOLVED); + ASSERT_ERROR(sd_future_result(group), ECANCELED); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_group_set_policy(group, policy)), ESTALE); + } +} + +TEST(future_group_size) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *first = NULL, *second = NULL; + + ASSERT_EQ(sd_future_group_size(/* f= */ NULL), 0U); + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_EQ(sd_future_group_size(group), 0U); + + ASSERT_OK(sd_future_new_defer(e, 0, &first)); + ASSERT_OK(sd_future_new_defer(e, 0, &second)); + ASSERT_EQ(ASSERT_RETURN_EXPECTED(sd_future_group_size(first)), 0U); + + ASSERT_OK(sd_future_group_add(group, first)); + ASSERT_EQ(sd_future_group_size(group), 1U); + ASSERT_OK(sd_future_group_add(group, second)); + ASSERT_EQ(sd_future_group_size(group), 2U); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(group)); + ASSERT_EQ(sd_future_group_size(group), 2U); +} + +/* Distinct errors pin down selection by insertion order when multiple children have settled + * before the group's callbacks run, including when completion order is reversed. */ +TEST(future_group_first_error) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + uint64_t policy; + + ASSERT_OK(sd_event_new(&e)); + + FOREACH_ARGUMENT(policy, SD_FUTURE_GROUP_WAIT_ALL, SD_FUTURE_GROUP_WAIT_ANY, + SD_FUTURE_GROUP_IGNORE_ERRORS, + SD_FUTURE_GROUP_WAIT_ANY | SD_FUTURE_GROUP_IGNORE_ERRORS) { + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *a = NULL, *b = NULL; + + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_group_set_policy(group, policy)); + ASSERT_OK(sd_future_group_new(e, &a)); + ASSERT_OK(sd_future_group_new(e, &b)); + ASSERT_OK(sd_future_group_add_many(group, a, b)); + ASSERT_OK(sd_future_resolve(b, -EIO)); + ASSERT_OK(sd_future_resolve(a, -EINVAL)); + + while (sd_future_state(group) == SD_FUTURE_PENDING) + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + + ASSERT_ERROR(sd_future_result(group), EINVAL); + } +} + +/* WAIT_ALL happy path: three children all succeed. */ +TEST(future_group_wait_all_happy) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *c1 = NULL, *c2 = NULL, *c3 = NULL; + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_new_defer(e, 0, &c1)); + ASSERT_OK(sd_future_new_defer(e, 0, &c2)); + ASSERT_OK(sd_future_new_defer(e, 0, &c3)); + ASSERT_OK(sd_future_group_add(group, c1)); + ASSERT_OK(sd_future_group_add(group, c2)); + ASSERT_OK(sd_future_group_add(group, c3)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(group)); + ASSERT_OK_ZERO(sd_future_result(c1)); + ASSERT_OK_ZERO(sd_future_result(c2)); + ASSERT_OK_ZERO(sd_future_result(c3)); +} + +/* WAIT_ALL fail-fast (default): one child errors fast, others sleeping; group resolves with + * that error, sleepers observe -ECANCELED. */ +TEST(future_group_wait_all_fail_fast) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *errorer = NULL, *sleeper_a = NULL, *sleeper_b = NULL; + ASSERT_OK(sd_future_group_new(e, &group)); + + ASSERT_OK(sd_future_new_defer(e, -EINVAL, &errorer)); + ASSERT_OK(sd_fiber_new(e, "sleep-a", suspend_fiber, NULL, NULL, &sleeper_a)); + ASSERT_OK(sd_fiber_new(e, "sleep-b", suspend_fiber, NULL, NULL, &sleeper_b)); + + ASSERT_OK(sd_future_group_add(group, errorer)); + ASSERT_OK(sd_future_group_add(group, sleeper_a)); + ASSERT_OK(sd_future_group_add(group, sleeper_b)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(group), EINVAL); + ASSERT_ERROR(sd_future_result(errorer), EINVAL); + ASSERT_ERROR(sd_future_result(sleeper_a), ECANCELED); + ASSERT_ERROR(sd_future_result(sleeper_b), ECANCELED); +} + +/* WAIT_ALL with IGNORE_ERRORS: one child errors, others succeed; everyone runs to completion; + * group resolves with the first error. */ +TEST(future_group_wait_all_ignore_errors) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *errorer = NULL, *succeeder = NULL; + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_group_set_policy(group, SD_FUTURE_GROUP_IGNORE_ERRORS)); + + ASSERT_OK(sd_future_new_defer(e, -EINVAL, &errorer)); + ASSERT_OK(sd_future_new_defer(e, 0, &succeeder)); + ASSERT_OK(sd_future_group_add(group, errorer)); + ASSERT_OK(sd_future_group_add(group, succeeder)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(group), EINVAL); + ASSERT_ERROR(sd_future_result(errorer), EINVAL); + ASSERT_OK_ZERO(sd_future_result(succeeder)); +} + +/* WAIT_ANY: three sleepers with different durations; group resolves with shortest sleeper's + * result; siblings observe -ECANCELED. */ +TEST(future_group_wait_any) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *fast = NULL, *medium = NULL, *slow = NULL; + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_group_set_policy(group, SD_FUTURE_GROUP_WAIT_ANY)); + + ASSERT_OK(sd_future_new_defer(e, 42, &fast)); + ASSERT_OK(sd_fiber_new(e, "medium", suspend_fiber, NULL, NULL, &medium)); + ASSERT_OK(sd_fiber_new(e, "slow", suspend_fiber, NULL, NULL, &slow)); + ASSERT_OK(sd_future_group_add(group, fast)); + ASSERT_OK(sd_future_group_add(group, medium)); + ASSERT_OK(sd_future_group_add(group, slow)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(sd_future_result(group), 42); + ASSERT_EQ(sd_future_result(fast), 42); + ASSERT_ERROR(sd_future_result(medium), ECANCELED); + ASSERT_ERROR(sd_future_result(slow), ECANCELED); +} + +TEST(future_group_wait_any_mixed_outcomes) { + bool success_first, resolve_success_first; + + FOREACH_ARGUMENT(success_first, false, true) + FOREACH_ARGUMENT(resolve_success_first, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *success = NULL, *error = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_group_set_policy(group, SD_FUTURE_GROUP_WAIT_ANY)); + ASSERT_OK(sd_future_group_new(e, &success)); + ASSERT_OK(sd_future_group_new(e, &error)); + ASSERT_OK(sd_future_group_add_many(group, + success_first ? success : error, + success_first ? error : success)); + + /* Settle both before dispatching either callback: success must win regardless + * of insertion or resolution order, even without IGNORE_ERRORS. */ + if (resolve_success_first) { + ASSERT_OK(sd_future_resolve(success, 42)); + ASSERT_OK(sd_future_resolve(error, -EIO)); + } else { + ASSERT_OK(sd_future_resolve(error, -EIO)); + ASSERT_OK(sd_future_resolve(success, 42)); + } + + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(sd_future_result(group), 42); + ASSERT_EQ(sd_future_result(success), 42); + ASSERT_ERROR(sd_future_result(error), EIO); + } +} + +/* WAIT_ANY|IGNORE_ERRORS (FIRST_SUCCESS): fast errorer, slower success, slowest pending; + * group resolves with the success value; slowest is cancelled. */ +TEST(future_group_first_success) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *fast_err = NULL, *medium_ok = NULL, *slow_ok = NULL; + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_group_set_policy(group, SD_FUTURE_GROUP_WAIT_ANY|SD_FUTURE_GROUP_IGNORE_ERRORS)); + + ASSERT_OK(sd_future_new_defer(e, -EINVAL, &fast_err)); + ASSERT_OK(sd_future_new_defer(e, 77, &medium_ok)); + ASSERT_OK(sd_fiber_new(e, "slow-ok", suspend_fiber, NULL, NULL, &slow_ok)); + + ASSERT_OK(sd_future_group_add(group, fast_err)); + ASSERT_OK(sd_future_group_add(group, medium_ok)); + ASSERT_OK(sd_future_group_add(group, slow_ok)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(sd_future_result(group), 77); + ASSERT_ERROR(sd_future_result(fast_err), EINVAL); + ASSERT_EQ(sd_future_result(medium_ok), 77); + ASSERT_ERROR(sd_future_result(slow_ok), ECANCELED); +} + +/* WAIT_ANY|IGNORE_ERRORS, all fail: every child errors; group resolves with first error. */ +TEST(future_group_first_success_all_fail) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *err_a = NULL, *err_b = NULL; + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_group_set_policy(group, SD_FUTURE_GROUP_WAIT_ANY|SD_FUTURE_GROUP_IGNORE_ERRORS)); + + ASSERT_OK(sd_future_new_defer(e, -EINVAL, &err_a)); + ASSERT_OK(sd_future_new_defer(e, -EIO, &err_b)); + ASSERT_OK(sd_future_group_add(group, err_a)); + ASSERT_OK(sd_future_group_add(group, err_b)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(group), EINVAL); + ASSERT_ERROR(sd_future_result(err_a), EINVAL); + ASSERT_ERROR(sd_future_result(err_b), EIO); +} + +/* External cancellation: long-running children + a deferred event source that cancels the + * group; group and every child observe -ECANCELED. */ +static int cancel_trigger(sd_event_source *src, void *userdata) { + sd_future *group = ASSERT_PTR(userdata); + return sd_future_cancel(group); +} + +TEST(future_group_external_cancel) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *long_a = NULL, *long_b = NULL; + ASSERT_OK(sd_future_group_new(e, &group)); + + ASSERT_OK(sd_fiber_new(e, "long-a", suspend_fiber, NULL, NULL, &long_a)); + ASSERT_OK(sd_fiber_new(e, "long-b", suspend_fiber, NULL, NULL, &long_b)); + ASSERT_OK(sd_future_group_add(group, long_a)); + ASSERT_OK(sd_future_group_add(group, long_b)); + + /* Deferred event source runs after fibers have had a chance to suspend, then cancels + * the group from outside the fiber stack. */ + _cleanup_(sd_event_source_unrefp) sd_event_source *cancel_src = NULL; + ASSERT_OK(sd_event_add_defer(e, &cancel_src, cancel_trigger, group)); + ASSERT_OK(sd_event_source_set_priority(cancel_src, 100)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(group), ECANCELED); + ASSERT_ERROR(sd_future_result(long_a), ECANCELED); + ASSERT_ERROR(sd_future_result(long_b), ECANCELED); +} + +/* Drain invariant: a fail-fast group with a fast errorer + a still-sleeping sibling. The + * group's done callback must see *every* child in RESOLVED state — the group may not + * settle while any cancelled child is still draining. */ +typedef struct DrainCheckState { + sd_future *child_a; + sd_future *child_b; + int a_state_at_resolve; + int b_state_at_resolve; +} DrainCheckState; + +static int drain_check_cb(sd_future *f, void *userdata) { + DrainCheckState *s = ASSERT_PTR(userdata); + s->a_state_at_resolve = sd_future_state(s->child_a); + s->b_state_at_resolve = sd_future_state(s->child_b); + return 0; +} + +TEST(future_group_resolves_after_children_drain) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *errorer = NULL, *sleeper = NULL; + ASSERT_OK(sd_future_group_new(e, &group)); + + /* The errorer yields once so the sleeper's dispatch fires first and the sleeper + * actually enters its body before the errorer returns. That way the group's cancel + * of the sleeper goes through the async (FIBER_STATE_SUSPENDED) path, not the + * synchronous FIBER_STATE_INITIAL path — which is the case the drain invariant is + * about. */ + int err_result = -EINVAL; + ASSERT_OK(sd_fiber_new(e, "err", yield_then_return_fiber, &err_result, NULL, &errorer)); + ASSERT_OK(sd_fiber_new(e, "sleep", suspend_fiber, NULL, NULL, &sleeper)); + ASSERT_OK(sd_future_group_add(group, errorer)); + ASSERT_OK(sd_future_group_add(group, sleeper)); + + DrainCheckState s = { .child_a = errorer, .child_b = sleeper }; + _cleanup_(sd_future_slot_unrefp) sd_future_slot *slot = NULL; + ASSERT_OK(sd_future_add_callback(group, &slot, drain_check_cb, &s)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_EQ(s.a_state_at_resolve, (int) SD_FUTURE_RESOLVED); + ASSERT_EQ(s.b_state_at_resolve, (int) SD_FUTURE_RESOLVED); + ASSERT_ERROR(sd_future_result(group), EINVAL); + ASSERT_ERROR(sd_future_result(sleeper), ECANCELED); +} + +/* Parent cancellation: a fiber creates a group with one errorer, then suspends without ever + * awaiting the group. When the errorer fails, the parent fiber's in-flight suspend must + * return -ECANCELED. With IGNORE_ERRORS the parent is left alone (a wake-up callback on + * the group lifts the suspend so we don't hang). */ +typedef struct ParentCancelState { + uint64_t policy; + int suspend_result; + int group_result; +} ParentCancelState; + +static int wake_parent_cb(sd_future *f, void *userdata) { + return sd_fiber_resume(userdata, 0); +} + +static int parent_cancel_driver(void *userdata) { + ParentCancelState *s = ASSERT_PTR(userdata); + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *errorer = NULL; + + ASSERT_OK(sd_future_group_new(sd_fiber_get_event(), &group)); + if (s->policy) + ASSERT_OK(sd_future_group_set_policy(group, s->policy)); + ASSERT_OK(sd_future_new_defer(sd_fiber_get_event(), -EINVAL, &errorer)); + ASSERT_OK(sd_future_group_add(group, errorer)); + + /* Wake-up slot so we don't hang in the IGNORE_ERRORS case (where the parent isn't + * cancelled). In the fail-fast case the queued -ECANCELED takes precedence over this + * callback's normal resume value. */ + _cleanup_(sd_future_slot_unrefp) sd_future_slot *wake_slot = NULL; + ASSERT_OK(sd_future_add_callback(group, &wake_slot, wake_parent_cb, sd_fiber_get_current())); + + s->suspend_result = sd_fiber_suspend(); + s->group_result = sd_future_result(group); + return 0; +} + +TEST(future_group_cancels_parent_on_child_error) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + ParentCancelState s = {}; + _cleanup_(sd_future_unrefp) sd_future *driver = NULL; + ASSERT_OK(sd_fiber_new(e, "parent", parent_cancel_driver, &s, NULL, &driver)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(s.suspend_result, ECANCELED); + ASSERT_ERROR(s.group_result, EINVAL); +} + +/* Awaiting a group must return the child's error instead of cancelling the waiting parent. */ +static int await_gets_error_driver(void *userdata) { + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *errorer = NULL; + int *await_result = ASSERT_PTR(userdata); + + ASSERT_OK(sd_future_group_new(sd_fiber_get_event(), &group)); + ASSERT_OK(sd_future_new_defer(sd_fiber_get_event(), -EINVAL, &errorer)); + ASSERT_OK(sd_future_group_add(group, errorer)); + + *await_result = sd_fiber_await(group); + ASSERT_OK_ZERO(sd_fiber_yield()); + return 0; +} + +TEST(future_group_await_returns_real_error) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *driver = NULL; + int await_result = 0; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_fiber_new(e, "parent", await_gets_error_driver, &await_result, /* destroy= */ NULL, &driver)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(driver)); + ASSERT_ERROR(await_result, EINVAL); +} + +TEST(future_group_does_not_cancel_parent_with_ignore_errors) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + ParentCancelState s = { .policy = SD_FUTURE_GROUP_IGNORE_ERRORS }; + _cleanup_(sd_future_unrefp) sd_future *driver = NULL; + ASSERT_OK(sd_fiber_new(e, "parent", parent_cancel_driver, &s, NULL, &driver)); + + ASSERT_OK(sd_event_loop(e)); + /* With IGNORE_ERRORS the parent isn't cancelled — the wake-up callback resumes the + * suspend with 0, and we read the group's error via sd_future_result. */ + ASSERT_OK_ZERO(s.suspend_result); + ASSERT_ERROR(s.group_result, EINVAL); +} + +/* Add already-resolved child to a default group: group stays PENDING until the next event-loop + * tick (via sd_future_add_callback's RESOLVED-on-fiber defer path), then resolves with the + * child's result. */ +typedef struct AddResolvedState { + sd_future *child; + sd_future *group; + int add_result; + int group_state_after_add; + int await_result; +} AddResolvedState; + +static int add_resolved_driver(void *userdata) { + AddResolvedState *s = ASSERT_PTR(userdata); + + /* Drive the pre-built child to completion. The defer resolves with -EINVAL, which + * makes sd_fiber_await return -EINVAL — we don't ASSERT_OK that. The child is now + * RESOLVED and we can add it to the group. */ + (void) sd_fiber_await(s->child); + ASSERT_EQ(sd_future_state(s->child), SD_FUTURE_RESOLVED); + + s->add_result = sd_future_group_add(s->group, s->child); + s->group_state_after_add = sd_future_state(s->group); + + /* Await drives the loop one more tick so the defer that wraps the RESOLVED child can + * fire group_child_resolved and settle the group. */ + s->await_result = sd_fiber_await(s->group); + return 0; +} + +TEST(future_group_add_resolved_child) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + AddResolvedState s = {}; + ASSERT_OK(sd_future_group_new(e, &s.group)); + ASSERT_OK(sd_future_new_defer(e, -EINVAL, &s.child)); + + _cleanup_(sd_future_unrefp) sd_future *driver = NULL; + ASSERT_OK(sd_fiber_new(e, "driver", add_resolved_driver, &s, NULL, &driver)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK(s.add_result); + ASSERT_EQ(s.group_state_after_add, SD_FUTURE_PENDING); + ASSERT_ERROR(s.await_result, EINVAL); + ASSERT_ERROR(sd_future_result(s.group), EINVAL); + s.child = sd_future_unref(s.child); + s.group = sd_future_unref(s.group); +} + +/* add_many: convenience that adds multiple children in one call, behaves like add. */ +typedef struct AddManyState { + sd_future *a; + sd_future *b; + sd_future *c; + sd_future *group; + int join_result; +} AddManyState; + +static int add_many_driver(void *userdata) { + AddManyState *s = ASSERT_PTR(userdata); + ASSERT_OK(sd_future_group_add_many(s->group, s->a, s->b, s->c)); + s->join_result = sd_fiber_await(s->group); + return 0; +} + +TEST(future_group_add_many) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + AddManyState s = {}; + ASSERT_OK(sd_future_group_new(e, &s.group)); + ASSERT_OK(sd_future_new_defer(e, 0, &s.a)); + ASSERT_OK(sd_future_new_defer(e, 0, &s.b)); + ASSERT_OK(sd_future_new_defer(e, 0, &s.c)); + + _cleanup_(sd_future_unrefp) sd_future *driver = NULL; + ASSERT_OK(sd_fiber_new(e, "driver", add_many_driver, &s, NULL, &driver)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(s.join_result); + + s.a = sd_future_unref(s.a); + s.b = sd_future_unref(s.b); + s.c = sd_future_unref(s.c); + s.group = sd_future_unref(s.group); +} + +/* Custom future ops whose cancel is a no-op on the first call and resolves on the second. + * Used to drive a group into the "finalizing but draining" state synchronously: one cancel + * of the group runs finalize, which calls our cancel once — but we don't resolve, so the + * group's state stays PENDING with finalizing=true. */ +typedef struct StubbornChild { + unsigned cancels; +} StubbornChild; + +static void* stubborn_child_alloc(void) { + return new0(StubbornChild, 1); +} + +static void stubborn_child_free(sd_future *f) { + free(sd_future_get_private(f)); +} + +static int stubborn_child_cancel(sd_future *f) { + StubbornChild *sc = ASSERT_PTR(sd_future_get_private(f)); + if (++sc->cancels >= 2) + return sd_future_resolve(f, -ECANCELED); + return 0; +} + +static const sd_future_ops stubborn_child_ops = { + .size = sizeof(sd_future_ops), + .alloc = stubborn_child_alloc, + .free = stubborn_child_free, + .cancel = stubborn_child_cancel, +}; + +static int failing_child_cancel(sd_future *f) { + StubbornChild *sc = ASSERT_PTR(sd_future_get_private(f)); + + sc->cancels++; + return -EIO; +} + +static const sd_future_ops cancel_failure_ops = { + .size = sizeof(sd_future_ops), + .alloc = stubborn_child_alloc, + .free = stubborn_child_free, + .cancel = failing_child_cancel, +}; + +TEST(future_group_cancel_failure_still_drains) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *failing = NULL, *sibling = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_new(e, &cancel_failure_ops, &failing)); + ASSERT_OK(sd_future_group_new(e, &sibling)); + ASSERT_OK(sd_future_group_add_many(group, failing, sibling)); + + ASSERT_ERROR(sd_future_cancel(group), EIO); + StubbornChild *sc = sd_future_get_private(failing); + ASSERT_EQ(sc->cancels, 1U); + ASSERT_EQ(sd_future_state(failing), SD_FUTURE_PENDING); + ASSERT_ERROR(sd_future_result(sibling), ECANCELED); + ASSERT_EQ(sd_future_group_size(group), 2U); + + /* The sibling's callback cannot finish the group while the failed cancellation is pending. */ + while (ASSERT_OK(sd_event_run(e, 0)) > 0) + ; + ASSERT_EQ(sd_future_state(group), SD_FUTURE_PENDING); + + ASSERT_OK(sd_future_resolve(failing, 42)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_ERROR(sd_future_result(group), ECANCELED); + ASSERT_EQ(sd_future_result(failing), 42); +} + +/* Cancelling a group with a still-draining child puts the group into the finalizing-but-PENDING + * state. While in that state, sd_future_group_add() must reject with -ESTALE — a freshly-added + * child would have missed the cancel loop and hang us forever waiting for it to settle. */ +TEST(future_group_add_rejected_during_finalize) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *stubborn = NULL, *new_child = NULL; + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_new(e, &stubborn_child_ops, &stubborn)); + ASSERT_OK(sd_future_group_add(group, stubborn)); + + /* First cancel: triggers finalize, calls stubborn's cancel (no-op #1, child still + * PENDING). Group is now finalizing=true with state PENDING. */ + ASSERT_OK(sd_future_cancel(group)); + ASSERT_EQ(sd_future_state(group), SD_FUTURE_PENDING); + ASSERT_EQ(sd_future_state(stubborn), SD_FUTURE_PENDING); + + ASSERT_OK(sd_future_new_defer(e, 0, &new_child)); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_group_add(group, new_child)), ESTALE); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_group_add_many(group, new_child)), ESTALE); + ASSERT_EQ(sd_future_group_size(group), 1U); + + /* Second cancel of stubborn resolves it, which on the next loop iteration drives + * group_child_resolved → future_group_check → group resolves with the locked-in + * -ECANCELED. */ + ASSERT_OK(sd_future_cancel(stubborn)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_ERROR(sd_future_result(group), ECANCELED); + ASSERT_ERROR(sd_future_result(stubborn), ECANCELED); + ASSERT_OK_ZERO(sd_future_result(new_child)); +} + +/* Policy must be configured before any children are added: once a child is in flight, the + * resolution mechanics are locked in. Multiple set_policy calls on a fresh group are fine. */ +TEST(future_group_set_policy_rejected_after_add) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + _cleanup_(sd_future_cancel_unrefp) sd_future *group = NULL; + _cleanup_(sd_future_unrefp) sd_future *child = NULL; + ASSERT_OK(sd_future_group_new(e, &group)); + + /* No children yet — set_policy can be called and re-called freely. */ + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_group_set_policy(group, UINT64_C(1) << 8)), EINVAL); + ASSERT_OK(sd_future_group_set_policy(group, SD_FUTURE_GROUP_WAIT_ANY)); + ASSERT_OK(sd_future_group_set_policy(group, + SD_FUTURE_GROUP_WAIT_ANY | SD_FUTURE_GROUP_IGNORE_ERRORS)); + + ASSERT_OK(sd_future_new_defer(e, 0, &child)); + ASSERT_OK(sd_future_group_add(group, child)); + + /* Now that a child is registered, set_policy must reject. */ + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_group_set_policy(group, SD_FUTURE_GROUP_WAIT_ALL)), ESTALE); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_group_set_policy(group, SD_FUTURE_GROUP_WAIT_ANY)), ESTALE); +} + +/* When the parent fiber itself drives the cancel of its own group, future_group_finalize() must + * skip the parent-cancel path — fiber_cancel asserts against self-cancellation, and even if that + * assertion were relaxed, queuing -ECANCELED on the parent would surface on a later suspend the + * caller didn't expect. */ +typedef struct SelfCancelState { + int cancel_return; + int group_result; + int yield_result; +} SelfCancelState; + +static int self_cancel_fiber(void *userdata) { + SelfCancelState *s = ASSERT_PTR(userdata); + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *child = NULL; + int r; + + r = sd_future_group_new(sd_fiber_get_event(), &group); + if (r < 0) + return r; + + r = sd_future_new_defer(sd_fiber_get_event(), 0, &child); + if (r < 0) + return r; + + r = sd_future_group_add(group, child); + if (r < 0) + return r; + + s->cancel_return = sd_future_cancel(group); + s->group_result = sd_future_result(group); + + /* If the parent-cancel guard didn't work, -ECANCELED would be queued on us by + * finalize() and surface here. Expect a clean yield (0). */ + s->yield_result = sd_fiber_yield(); + return 0; +} + +TEST(future_group_does_not_cancel_parent_when_parent_drives_cancel) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + + SelfCancelState s = {}; + _cleanup_(sd_future_unrefp) sd_future *driver = NULL; + ASSERT_OK(sd_fiber_new(e, "self-cancel", self_cancel_fiber, &s, NULL, &driver)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(driver)); + ASSERT_OK(s.cancel_return); + ASSERT_ERROR(s.group_result, ECANCELED); + ASSERT_OK_ZERO(s.yield_result); +} + +typedef struct ParentAwaitState { + sd_future *group; + sd_future *child; + sd_future *parent; + bool await; + bool ready; + int result; +} ParentAwaitState; + +static int parent_await_driver(void *userdata) { + ParentAwaitState *s = ASSERT_PTR(userdata); + + ASSERT_OK(sd_future_group_new(sd_fiber_get_event(), &s->group)); + ASSERT_OK(sd_future_group_new(sd_fiber_get_event(), &s->child)); + ASSERT_OK(sd_future_group_add(s->group, s->child)); + + if (s->await) + ASSERT_ERROR(sd_fiber_await(s->group), ECANCELED); + else + ASSERT_EQ(sd_fiber_suspend(), 42); + + s->ready = true; + ASSERT_EQ(sd_fiber_suspend(), s->result); + + /* Parent cancellation does not replace the group's result. A later await of the resolved + * group returns that result, just like sd_future_result(). */ + if (s->result == -ECANCELED) { + ASSERT_EQ(sd_future_state(s->group), SD_FUTURE_RESOLVED); + ASSERT_ERROR(sd_future_result(s->group), EIO); + ASSERT_ERROR(sd_fiber_await(s->group), EIO); + } + return 0; +} + +TEST(future_group_parent_await_scope) { + bool await; + + FOREACH_ARGUMENT(await, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *parent = NULL; + ParentAwaitState s = { .await = await, .result = -ECANCELED }; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_fiber_new(e, "parent-await", parent_await_driver, &s, + /* destroy= */ NULL, &parent)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + if (s.await) + ASSERT_OK(sd_future_cancel(parent)); + else { + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_fiber_await(s.group)), ESRCH); + ASSERT_OK(sd_fiber_resume(parent, 42)); + } + while (!s.ready) + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + + ASSERT_OK(sd_future_resolve(s.child, -EIO)); + while (ASSERT_OK(sd_event_run(e, 0)) > 0) + ; + + /* No exit-on-idle: only the child failure may wake the parent. */ + ASSERT_EQ(sd_future_state(parent), SD_FUTURE_RESOLVED); + ASSERT_OK_ZERO(sd_future_result(parent)); + ASSERT_ERROR(sd_future_result(s.group), EIO); + sd_future_unref(s.group); + sd_future_unref(s.child); + } +} + +static int cancel_group_from_peer(void *userdata) { + ParentAwaitState *s = ASSERT_PTR(userdata); + + sd_future_cancel_unref(sd_future_ref(s->group)); + return sd_fiber_resume(s->parent, 42); +} + +TEST(future_group_external_cancel_leaves_parent_running) { + bool peer; + + FOREACH_ARGUMENT(peer, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *parent = NULL, *canceller = NULL; + ParentAwaitState s = { .result = 42 }; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_fiber_new(e, "parent", parent_await_driver, &s, + /* destroy= */ NULL, &parent)); + s.parent = parent; + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_OK(sd_fiber_resume(parent, 42)); + while (!s.ready) + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + + if (peer) + ASSERT_OK(sd_fiber_new(e, "cancel-group", cancel_group_from_peer, &s, + /* destroy= */ NULL, &canceller)); + else + ASSERT_OK(cancel_group_from_peer(&s)); + + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(parent)); + if (peer) + ASSERT_OK_ZERO(sd_future_result(canceller)); + ASSERT_ERROR(sd_future_result(s.group), ECANCELED); + sd_future_unref(s.group); + sd_future_unref(s.child); + } +} + +static int peer_await_driver(void *userdata) { + return sd_fiber_await(userdata); +} + +TEST(future_group_peer_await_does_not_suppress_parent_cancel) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *parent = NULL, *peer = NULL; + ParentAwaitState s = { .result = -ECANCELED }; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_fiber_new(e, "parent", parent_await_driver, &s, + /* destroy= */ NULL, &parent)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_OK(sd_fiber_resume(parent, 42)); + while (!s.ready) + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + + ASSERT_OK(sd_fiber_new(e, "peer-await", peer_await_driver, s.group, + /* destroy= */ NULL, &peer)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_OK(sd_future_resolve(s.child, -EIO)); + while (ASSERT_OK(sd_event_run(e, 0)) > 0) + ; + + ASSERT_EQ(sd_future_state(parent), SD_FUTURE_RESOLVED); + ASSERT_OK_ZERO(sd_future_result(parent)); + ASSERT_ERROR(sd_future_result(peer), EIO); + sd_future_unref(s.group); + sd_future_unref(s.child); +} + +TEST(future_group_priority) { + bool before_add; + + FOREACH_ARGUMENT(before_add, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *pending = NULL, *resolved = NULL, + *unsupported = NULL, *probe = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_group_new(e, &resolved)); + ASSERT_OK(sd_future_resolve(resolved, 0)); + ASSERT_OK(sd_future_new(e, &stubborn_child_ops, &unsupported)); + ASSERT_OK(sd_future_new_defer(e, 0, &probe)); + ASSERT_OK(sd_future_new_defer(e, 0, &pending)); + if (before_add) + ASSERT_OK(sd_future_set_priority(group, -10)); + ASSERT_OK(ASSERT_RETURN_EXPECTED(sd_future_group_add_many(group, resolved, unsupported, pending))); + if (!before_add) + ASSERT_OK(ASSERT_RETURN_EXPECTED(sd_future_set_priority(group, -10))); + + /* The pending child overtakes a source created earlier at normal priority. Resolved children + * and children without priority support must not make either operation fail. */ + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(pending), SD_FUTURE_RESOLVED); + ASSERT_EQ(sd_future_state(probe), SD_FUTURE_PENDING); + ASSERT_OK(sd_future_resolve(unsupported, 0)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(group)); + ASSERT_OK_ZERO(sd_future_result(probe)); + } +} + +TEST(future_group_priority_unset) { + bool explicit_priority; + + FOREACH_ARGUMENT(explicit_priority, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *pending = NULL, *probe = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_new_defer(e, 0, &pending)); + ASSERT_OK(sd_future_set_priority(pending, -10)); + ASSERT_OK(sd_future_new_defer(e, 0, &probe)); + ASSERT_OK(sd_future_set_priority(probe, -5)); + if (explicit_priority) + ASSERT_OK(sd_future_set_priority(group, 0)); + ASSERT_OK(sd_future_group_add(group, pending)); + + /* An unset group priority preserves the child's priority; explicitly setting zero + * overrides it, making the probe run first. */ + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(pending), explicit_priority ? SD_FUTURE_PENDING : SD_FUTURE_RESOLVED); + ASSERT_EQ(sd_future_state(probe), explicit_priority ? SD_FUTURE_RESOLVED : SD_FUTURE_PENDING); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(group)); + ASSERT_OK_ZERO(sd_future_result(probe)); + } +} + +static int child_set_priority_fail(sd_future *f, int64_t priority) { + return -EIO; +} + +static const sd_future_ops priority_failure_ops = { + .size = sizeof(sd_future_ops), + .alloc = stubborn_child_alloc, + .free = stubborn_child_free, + .cancel = stubborn_child_cancel, + .set_priority = child_set_priority_fail, +}; + +TEST(future_group_priority_add_failure) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_cancel_unrefp) sd_future *group = NULL; + _cleanup_(sd_future_unrefp) sd_future *child = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_set_priority(group, -10)); + ASSERT_OK(sd_future_new(e, &priority_failure_ops, &child)); + ASSERT_ERROR(sd_future_group_add(group, child), EIO); + ASSERT_EQ(sd_future_group_size(group), 0U); + + /* A failed add must leave neither membership nor a callback behind. */ + ASSERT_OK(sd_future_resolve(child, 0)); + ASSERT_OK_ZERO(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(group), SD_FUTURE_PENDING); +} + +TEST(future_group_priority_failure) { + bool failing_first; + + FOREACH_ARGUMENT(failing_first, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *accepting = NULL, *failing = NULL, + *later = NULL, *probe = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_new(e, &priority_failure_ops, &failing)); + ASSERT_OK(sd_future_new_defer(e, 0, &probe)); + ASSERT_OK(sd_future_set_priority(probe, -5)); + ASSERT_OK(sd_future_new_defer(e, 0, &accepting)); + ASSERT_OK(sd_future_group_add_many(group, + failing_first ? failing : accepting, + failing_first ? accepting : failing)); + ASSERT_ERROR(sd_future_set_priority(group, -10), EIO); + + /* A failure neither stops updates to the other children nor installs the attempted + * priority as the group's default for later additions. */ + ASSERT_OK(sd_future_new_defer(e, 0, &later)); + ASSERT_OK(sd_future_group_add(group, later)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(accepting), SD_FUTURE_RESOLVED); + ASSERT_EQ(sd_future_state(probe), SD_FUTURE_PENDING); + ASSERT_EQ(sd_future_state(later), SD_FUTURE_PENDING); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(probe), SD_FUTURE_RESOLVED); + ASSERT_EQ(sd_future_state(later), SD_FUTURE_PENDING); + + ASSERT_OK(sd_future_resolve(failing, 0)); + ASSERT_OK(sd_event_set_exit_on_idle(e, true)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(group)); + ASSERT_OK_ZERO(sd_future_result(later)); + } +} + +TEST(future_group_add_many_rollback) { + bool existing; + + FOREACH_ARGUMENT(existing, false, true) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *group = NULL, *original = NULL, + *first = NULL, *second = NULL, *failing = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_group_set_policy(group, SD_FUTURE_GROUP_WAIT_ANY)); + ASSERT_OK(sd_future_set_priority(group, -10)); + if (existing) { + ASSERT_OK(sd_future_group_new(e, &original)); + ASSERT_OK(sd_future_group_add(group, original)); + } + ASSERT_OK(sd_future_new_defer(e, 0, &first)); + ASSERT_OK(sd_future_new_defer(e, 0, &second)); + ASSERT_OK(sd_future_new(e, &priority_failure_ops, &failing)); + + ASSERT_ERROR(sd_future_group_add_many(group, first, second, failing), EIO); + ASSERT_EQ(sd_future_group_size(group), (size_t) existing); + ASSERT_OK(sd_future_resolve(failing, -EIO)); + + /* Only the two defer futures may dispatch: every partially registered callback must + * be removed, and any pre-existing membership must survive the rollback. */ + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_OK_ZERO(sd_event_run(e, 0)); + ASSERT_OK_ZERO(sd_future_result(first)); + ASSERT_OK_ZERO(sd_future_result(second)); + ASSERT_EQ(sd_future_state(group), SD_FUTURE_PENDING); + + if (existing) + ASSERT_OK(sd_future_resolve(original, 42)); + else + ASSERT_OK(ASSERT_RETURN_EXPECTED(sd_future_group_add(group, first))); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_result(group), existing ? 42 : 0); + } +} + +TEST(future_group_rejects_cross_event_child) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL, *other = NULL; + _cleanup_(sd_future_cancel_unrefp) sd_future *group = NULL; + _cleanup_(sd_future_unrefp) sd_future *child = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_new(&other)); + ASSERT_OK(sd_future_group_new(e, &group)); + ASSERT_OK(sd_future_group_new(other, &child)); + + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_group_add(group, child)), EINVAL); + ASSERT_OK(sd_future_resolve(child, -EIO)); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_group_add(group, child)), EINVAL); + ASSERT_EQ(sd_future_group_size(group), 0U); + ASSERT_OK_ZERO(sd_event_run(e, 0)); + ASSERT_OK_ZERO(sd_event_run(other, 0)); + ASSERT_EQ(sd_future_state(group), SD_FUTURE_PENDING); +} + +TEST(future_group_rejects_self) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_cancel_unrefp) sd_future *group = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_future_group_new(e, &group)); + + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_group_add(group, group)), EINVAL); + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(sd_future_group_add_many(group, group)), EINVAL); + ASSERT_EQ(sd_future_group_size(group), 0U); + ASSERT_OK_ZERO(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(group), SD_FUTURE_PENDING); +} + +typedef struct CrossEventParentState { + sd_event *event; + sd_future *group; +} CrossEventParentState; + +static int cross_event_parent_driver(void *userdata) { + CrossEventParentState *s = ASSERT_PTR(userdata); + + ASSERT_OK(sd_future_group_new(s->event, &s->group)); + ASSERT_EQ(sd_fiber_suspend(), 42); + return 0; +} + +TEST(future_group_does_not_capture_cross_event_parent) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL, *other = NULL; + _cleanup_(sd_future_unrefp) sd_future *parent = NULL, *child = NULL; + CrossEventParentState s = {}; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_event_new(&other)); + s.event = other; + ASSERT_OK(sd_future_group_new(other, &child)); + ASSERT_OK(sd_fiber_new(e, "cross-event-parent", cross_event_parent_driver, &s, + /* destroy= */ NULL, &parent)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_OK(sd_future_group_add(s.group, child)); + ASSERT_OK(sd_future_resolve(child, -EIO)); + while (ASSERT_OK(sd_event_run(other, 0)) > 0) + ; + ASSERT_ERROR(sd_future_result(s.group), EIO); + + /* A failure on the other loop must neither schedule nor cancel the creating fiber. */ + ASSERT_OK_ZERO(sd_event_run(e, 0)); + ASSERT_EQ(sd_future_state(parent), SD_FUTURE_PENDING); + ASSERT_OK(sd_fiber_resume(parent, 42)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_OK_ZERO(sd_future_result(parent)); + sd_future_unref(s.group); +} + +typedef struct ParentLifetimeState { + sd_future *group; + sd_future *child; + bool parent_freed; +} ParentLifetimeState; + +static int parent_returns_before_child(void *userdata) { + ParentLifetimeState *s = ASSERT_PTR(userdata); + + ASSERT_OK(sd_future_group_new(sd_fiber_get_event(), &s->group)); + ASSERT_OK(sd_future_group_new(sd_fiber_get_event(), &s->child)); + ASSERT_OK(sd_future_group_add(s->group, s->child)); + return 0; +} + +static void parent_lifetime_destroy(void *userdata) { + ParentLifetimeState *s = ASSERT_PTR(userdata); + + s->parent_freed = true; +} + +TEST(future_group_outlives_parent) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *parent = NULL; + ParentLifetimeState s = {}; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_fiber_new(e, "short-lived-parent", parent_returns_before_child, &s, + parent_lifetime_destroy, &parent)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_OK_ZERO(sd_future_result(parent)); + parent = sd_future_unref(parent); + + /* Dispatch the parent-resolution callbacks before the child fails. They must release their + * references and forget the parent, since a later child error must not cancel freed memory. */ + while (ASSERT_OK(sd_event_run(e, 0)) > 0) + ; + ASSERT_TRUE(s.parent_freed); + ASSERT_EQ(sd_future_state(s.group), SD_FUTURE_PENDING); + ASSERT_OK(sd_future_resolve(s.child, -EIO)); + ASSERT_OK_POSITIVE(sd_event_run(e, 0)); + ASSERT_ERROR(sd_future_result(s.group), EIO); + sd_future_unref(s.group); + sd_future_unref(s.child); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/libsystemd/sd-varlink/sd-varlink.c b/src/libsystemd/sd-varlink/sd-varlink.c index ebf881459a..d5385f0ad8 100644 --- a/src/libsystemd/sd-varlink/sd-varlink.c +++ b/src/libsystemd/sd-varlink/sd-varlink.c @@ -1164,7 +1164,7 @@ static int varlink_dispatch_fiber(sd_varlink *v, const char *method, sd_varlink_ .callback = callback, }; - _cleanup_(sd_future_unrefp) sd_future *f = NULL; + _cleanup_(sd_future_cancel_unrefp) sd_future *f = NULL; r = sd_fiber_new(v->server->event, method, varlink_fiber_entry, d, varlink_fiber_data_destroy, &f); if (r < 0) return r; @@ -1189,6 +1189,8 @@ static int varlink_dispatch_fiber(sd_varlink *v, const char *method, sd_varlink_ if (r < 0) return r; + /* Floating self-ref keeps the fiber alive; release our local ref without cancelling. */ + f = sd_future_unref(f); return 0; } diff --git a/src/shared/qmp-client.c b/src/shared/qmp-client.c index 89b8551126..42e7eb32bd 100644 --- a/src/shared/qmp-client.c +++ b/src/shared/qmp-client.c @@ -908,9 +908,10 @@ int qmp_client_call_future( assert(c); assert(command); assert(ret); + assert_return(qmp_client_get_event(c), -ENOPKG); - _cleanup_(sd_future_unrefp) sd_future *f = NULL; - r = sd_future_new(&qmp_call_future_ops, &f); + _cleanup_(sd_future_cancel_unrefp) sd_future *f = NULL; + r = sd_future_new(qmp_client_get_event(c), &qmp_call_future_ops, &f); if (r < 0) return r; @@ -976,7 +977,7 @@ static int qmp_client_call_suspend( if (r < 0) return r; - r = sd_fiber_suspend(); + r = sd_fiber_await(call); /* If the future isn't resolved, the suspend was interrupted before a reply arrived (fiber * cancelled, fiber-wide SD_FIBER_TIMEOUT scope expired, …). There's no reply to extract, diff --git a/src/systemd/sd-future.h b/src/systemd/sd-future.h index 5e0fa22525..93b76e4f50 100644 --- a/src/systemd/sd-future.h +++ b/src/systemd/sd-future.h @@ -32,7 +32,8 @@ struct timespec; typedef struct sd_event sd_event; typedef struct sd_future sd_future; typedef struct sd_future_ops sd_future_ops; -typedef int (*sd_future_func_t)(sd_future *f); +typedef struct sd_future_slot sd_future_slot; +typedef int (*sd_future_func_t)(sd_future *f, void *userdata); typedef int (*sd_fiber_func_t)(void *userdata); typedef _sd_destroy_t sd_fiber_destroy_t; @@ -50,15 +51,27 @@ __extension__ typedef enum _SD_ENUM_TYPE_S64(sd_future_state_t) { _SD_ENUM_FORCE_S64(SD_FUTURE_STATE) } sd_future_state_t; -int sd_future_new(const sd_future_ops *ops, sd_future **ret); +/* SD_EVENT_DEFAULT selects the calling thread's existing default event loop, or fails with -ENOPKG. */ +int sd_future_new(sd_event *e, const sd_future_ops *ops, sd_future **ret); int sd_future_cancel(sd_future *f); int sd_future_resolve(sd_future *f, int result); +/* A future must be RESOLVED before its last reference is released; dropping the last reference to a + * PENDING future is a programming error that aborts the process. Use sd_future_cancel_unref() when + * cancellation resolves synchronously, or sd_future_cancel_wait_unref() from a fiber when it must + * wait for resolution. Plain sd_future_unref() does not cancel the future. */ _SD_DECLARE_TRIVIAL_REF_UNREF_FUNC(sd_future); _SD_DEFINE_POINTER_CLEANUP_FUNC(sd_future, sd_future_unref); void sd_future_unref_array_clear(sd_future *array[], size_t n); void sd_future_unref_array(sd_future *array[], size_t n); +sd_future* sd_future_cancel_unref(sd_future *f); +_SD_DEFINE_POINTER_CLEANUP_FUNC(sd_future, sd_future_cancel_unref); +void sd_future_cancel_unref_array_clear(sd_future *array[], size_t n); +void sd_future_cancel_unref_array(sd_future *array[], size_t n); + +/* Cancel and release the caller's reference after resolution. The target must not be the calling + * fiber. Asynchronous cancellation must be awaited from a fiber on the same event loop. */ sd_future* sd_future_cancel_wait_unref(sd_future *f); _SD_DEFINE_POINTER_CLEANUP_FUNC(sd_future, sd_future_cancel_wait_unref); void sd_future_cancel_wait_unref_array_clear(sd_future *array[], size_t n); @@ -66,14 +79,52 @@ void sd_future_cancel_wait_unref_array(sd_future *array[], size_t n); int sd_future_state(sd_future *f); int sd_future_result(sd_future *f); -void* sd_future_get_userdata(sd_future *f); void* sd_future_get_private(sd_future *f); const sd_future_ops* sd_future_get_ops(sd_future *f); +sd_event* sd_future_get_event(sd_future *f); + +int sd_future_add_callback(sd_future *f, sd_future_slot **ret_slot, sd_future_func_t callback, void *userdata); + +int sd_future_new_defer(sd_event *e, int result, sd_future **ret); + +_SD_DECLARE_TRIVIAL_REF_UNREF_FUNC(sd_future_slot); +_SD_DEFINE_POINTER_CLEANUP_FUNC(sd_future_slot, sd_future_slot_unref); + +sd_future* sd_future_slot_get_future(sd_future_slot *s); -int sd_future_set_callback(sd_future *f, sd_future_func_t callback, void *userdata); int sd_future_set_priority(sd_future *f, int64_t priority); -int sd_future_new_wait(sd_future *target, sd_future **ret); +/* Group policies select an outcome as follows: + * WAIT_ALL: first child error, or 0 once every child succeeds. + * WAIT_ALL | IGNORE_ERRORS: wait for every child, then return the first error or 0. + * WAIT_ANY: a successful child's result if any have succeeded, otherwise the first child error. + * WAIT_ANY | IGNORE_ERRORS: wait for a success, or for every child to fail; return that success or + * the first error, respectively. + * "First" means insertion order among the children already resolved when the outcome is selected. + * Once selected, the outcome is final: remaining children are cancelled, and the group resolves only + * after every child has finished. IGNORE_ERRORS also suppresses cancellation of the parent fiber; + * it does not turn an unsuccessful group result into success. Empty groups stay pending until cancelled. */ +__extension__ typedef enum _SD_ENUM_TYPE_S64(sd_future_group_policy_t) { + SD_FUTURE_GROUP_WAIT_ALL = 0, + SD_FUTURE_GROUP_WAIT_ANY = 1 << 0, + SD_FUTURE_GROUP_IGNORE_ERRORS = 1 << 1, + _SD_FUTURE_GROUP_POLICY_MASK = SD_FUTURE_GROUP_WAIT_ANY | SD_FUTURE_GROUP_IGNORE_ERRORS, + _SD_ENUM_FORCE_S64(SD_FUTURE_GROUP_POLICY) +} sd_future_group_policy_t; + +/* The calling fiber becomes the parent only if it belongs to e. Unless IGNORE_ERRORS is set, a child + * error cancels the parent if it is not awaiting the group with sd_fiber_await(). Explicit group + * cancellation does not cancel the parent. */ +int sd_future_group_new(sd_event *e, sd_future **ret); +int sd_future_group_set_policy(sd_future *f, uint64_t policy); +/* The group and all its children must belong to the same event loop. Adding a child takes a new + * reference; it does not consume the caller's reference. To hand ownership to the group, release the + * caller's reference with sd_future_unref(), not a cancellation cleanup helper. */ +int sd_future_group_add(sd_future *f, sd_future *child); +int sd_future_group_add_many_internal(sd_future *f, ...) _sd_sentinel_; +#define sd_future_group_add_many(f, ...) sd_future_group_add_many_internal(f, __VA_ARGS__, NULL) +/* NULL is treated as an empty group. */ +size_t sd_future_group_size(sd_future *f); int sd_fiber_new(sd_event *e, const char *name, sd_fiber_func_t func, void *userdata, sd_fiber_destroy_t destroy, sd_future **ret); @@ -84,9 +135,17 @@ int sd_fiber_is_running(void); sd_future* sd_fiber_get_current(void); int sd_fiber_get_priority(int64_t *ret); sd_event* sd_fiber_get_event(void); +/* The future a fiber is suspended in sd_fiber_await() or sd_future_cancel_wait_unref() for, if any. + * The pointer is borrowed and only valid until the fiber runs again. */ +sd_future* sd_fiber_get_awaiting(sd_future *f); int sd_fiber_yield(void); int sd_fiber_sleep(uint64_t usec); +/* Return the target's result on completion, or a wait error if the calling fiber is interrupted or + * the wait fails. An interrupted wait does not imply that the target has resolved; once it has, + * sd_future_result() gives its outcome independently of the wait's return value. An already-resolved + * target returns its result without consuming a pending interruption. The target must belong to the + * calling fiber's event loop. */ int sd_fiber_await(sd_future *target); int sd_fiber_suspend(void); int sd_fiber_resume(sd_future *f, int result); diff --git a/src/test/test-qmp-client.c b/src/test/test-qmp-client.c index 3615a8599a..f13a8913ed 100644 --- a/src/test/test-qmp-client.c +++ b/src/test/test-qmp-client.c @@ -329,6 +329,19 @@ TEST(qmp_call) { run_qmp_test(mock_qmp_call_fiber, qmp_client_call_fiber); } +TEST(qmp_call_future_without_event) { + _cleanup_(qmp_client_unrefp) QmpClient *client = NULL; + _cleanup_(sd_future_cancel_unrefp) sd_future *f = NULL; + _cleanup_close_pair_ int fds[2] = EBADF_PAIR; + + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, fds)); + ASSERT_OK(qmp_client_connect_fd(&client, TAKE_FD(fds[0]))); + ASSERT_NULL(qmp_client_get_event(client)); + + ASSERT_ERROR(ASSERT_RETURN_EXPECTED(qmp_client_call_future(client, "query-status", /* args= */ NULL, &f)), ENOPKG); + ASSERT_NULL(f); +} + static int mock_qmp_call_disconnect_fiber(void *userdata) { _cleanup_(json_stream_done) JsonStream s = {}; _cleanup_(sd_json_variant_unrefp) sd_json_variant *stop_cmd = NULL; @@ -354,6 +367,84 @@ TEST(qmp_call_disconnect) { run_qmp_test(mock_qmp_call_disconnect_fiber, qmp_client_call_disconnect_fiber); } +typedef struct QmpInterruptedCall { + int fd; + sd_future *caller; + int error; +} QmpInterruptedCall; + +static int mock_qmp_call_interrupted_fiber(void *userdata) { + QmpInterruptedCall *c = ASSERT_PTR(userdata); + _cleanup_(json_stream_done) JsonStream s = {}; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *stop_cmd = NULL, *cont_cmd = NULL; + + mock_qmp_init(&s, c->fd); + mock_qmp_handshake(&s); + mock_qmp_query_status_running(&s); + + sd_json_variant *stop_id = mock_qmp_expect(&s, "stop", &stop_cmd); + if (c->error == ECANCELED) + ASSERT_OK(sd_future_cancel(c->caller)); + else if (c->error == EBUSY) + ASSERT_OK(sd_fiber_resume(c->caller, 42)); + + /* Wait until the client has abandoned stop and issued another call. Delivering the late + * reply first must not wake the new wait or touch the cancelled call's freed future. */ + sd_json_variant *cont_id = mock_qmp_expect(&s, "cont", &cont_cmd); + mock_qmp_reply(&s, stop_id, /* reply_data= */ NULL); + mock_qmp_reply(&s, cont_id, /* reply_data= */ NULL); + return 0; +} + +static int qmp_client_call_interrupted_fiber(void *userdata) { + int error = *(int*) ASSERT_PTR(userdata); + _cleanup_(qmp_client_unrefp) QmpClient *client = NULL; + _cleanup_(sd_future_cancel_wait_unrefp) sd_future *mock = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL; + _cleanup_free_ char *error_desc = NULL; + _cleanup_close_pair_ int fds[2] = EBADF_PAIR; + + ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, fds)); + ASSERT_OK(qmp_client_connect_fd(&client, TAKE_FD(fds[0]))); + ASSERT_OK(qmp_client_attach_event(client, sd_fiber_get_event(), SD_EVENT_PRIORITY_NORMAL)); + QmpInterruptedCall c = { .fd = TAKE_FD(fds[1]), .caller = sd_fiber_get_current(), .error = error }; + ASSERT_OK(sd_fiber_new(sd_fiber_get_event(), "interrupted-mock", mock_qmp_call_interrupted_fiber, + &c, /* destroy= */ NULL, &mock)); + + /* Finish the handshake before testing interruption of a command awaiting its reply. */ + ASSERT_OK_POSITIVE(qmp_client_call(client, "query-status", /* args= */ NULL, + /* ret_result= */ NULL, /* reterr_error_desc= */ NULL)); + { + SD_FIBER_TIMEOUT(error == ETIME ? 0 : USEC_INFINITY); + ASSERT_ERROR(qmp_client_call(client, "stop", /* args= */ NULL, &result, &error_desc), error); + } + ASSERT_NULL(result); + ASSERT_NULL(error_desc); + + ASSERT_OK_POSITIVE(qmp_client_call(client, "cont", /* args= */ NULL, &result, &error_desc)); + ASSERT_NOT_NULL(result); + ASSERT_NULL(error_desc); + ASSERT_OK_ZERO(sd_fiber_await(mock)); + ASSERT_OK_ZERO(sd_fiber_yield()); + ASSERT_OK(sd_event_exit(sd_fiber_get_event(), 0)); + return 0; +} + +TEST(qmp_call_interrupted) { + int error; + + FOREACH_ARGUMENT(error, ECANCELED, ETIME, EBUSY) { + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + _cleanup_(sd_future_unrefp) sd_future *caller = NULL; + + ASSERT_OK(sd_event_new(&e)); + ASSERT_OK(sd_fiber_new(e, "interrupted-client", qmp_client_call_interrupted_fiber, + &error, /* destroy= */ NULL, &caller)); + ASSERT_OK(sd_event_loop(e)); + ASSERT_OK_ZERO(sd_future_result(caller)); + } +} + static int mock_qmp_fd_fiber(void *userdata) { _cleanup_(json_stream_done) JsonStream s = {}; _cleanup_(sd_json_variant_unrefp) sd_json_variant *cap_cmd = NULL, *addfd_cmd = NULL,