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,