Skip to content

Commit 5a051c0

Browse files
committed
Implement compat flag for queues to not block on waitUntil'ed work
1 parent c102caa commit 5a051c0

File tree

2 files changed

+77
-10
lines changed

2 files changed

+77
-10
lines changed

src/workerd/api/queue.c++

+75-10
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,9 @@ QueueMessage::QueueMessage(
356356
attempts(message.attempts),
357357
result(result) {}
358358

359-
jsg::JsValue QueueMessage::getBody(jsg::Lock& js) { return body.getHandle(js); }
359+
jsg::JsValue QueueMessage::getBody(jsg::Lock& js) {
360+
return body.getHandle(js);
361+
}
360362

361363
void QueueMessage::retry(jsg::Optional<QueueRetryOptions> options) {
362364
if (result->ackAll) {
@@ -459,15 +461,20 @@ void QueueEvent::ackAll() {
459461
}
460462

461463
namespace {
462-
jsg::Ref<QueueEvent> startQueueEvent(EventTarget& globalEventTarget,
464+
465+
struct StartQueueEventResponse {
466+
jsg::Ref<QueueEvent> event = nullptr;
467+
kj::Maybe<kj::Promise<void>> exportedHandlerProm;
468+
bool isServiceWorkerHandler = false;
469+
};
470+
471+
StartQueueEventResponse startQueueEvent(EventTarget& globalEventTarget,
463472
kj::OneOf<rpc::EventDispatcher::QueueParams::Reader, QueueEvent::Params> params,
464473
IoPtr<QueueEventResult> result,
465474
Worker::Lock& lock,
466475
kj::Maybe<ExportedHandler&> exportedHandler,
467476
const jsg::TypeHandler<QueueExportedHandler>& handlerHandler) {
468477
jsg::Lock& js = lock;
469-
// Start a queue event (called from C++, not JS). Similar to startScheduled(), the caller must
470-
// wait for waitUntil()s to produce the final QueueResult.
471478
jsg::Ref<QueueEvent> event(nullptr);
472479
KJ_SWITCH_ONEOF(params) {
473480
KJ_CASE_ONEOF(p, rpc::EventDispatcher::QueueParams::Reader) {
@@ -478,23 +485,31 @@ jsg::Ref<QueueEvent> startQueueEvent(EventTarget& globalEventTarget,
478485
}
479486
}
480487

488+
kj::Maybe<kj::Promise<void>> exportedHandlerProm;
489+
bool isServiceWorkerHandler = false;
481490
KJ_IF_SOME(h, exportedHandler) {
482491
auto queueHandler = KJ_ASSERT_NONNULL(handlerHandler.tryUnwrap(lock, h.self.getHandle(lock)));
483492
KJ_IF_SOME(f, queueHandler.queue) {
484493
auto promise = f(lock, jsg::alloc<QueueController>(event.addRef()),
485-
jsg::JsValue(h.env.getHandle(js)).addRef(js), h.getCtx());
486-
event->waitUntil(promise.then([event = event.addRef()]() mutable {
494+
jsg::JsValue(h.env.getHandle(js)).addRef(js), h.getCtx())
495+
.then([event = event.addRef()]() mutable {
487496
event->setCompletionStatus(QueueEvent::CompletedSuccessfully{});
488497
}, [event = event.addRef()](kj::Exception&& e) mutable {
489498
event->setCompletionStatus(QueueEvent::CompletedWithError{kj::cp(e)});
490499
return kj::mv(e);
491-
}));
500+
});
501+
if (FeatureFlags::get(js).getQueueConsumerNoWaitForWaitUntil()) {
502+
exportedHandlerProm = kj::mv(promise);
503+
} else {
504+
event->waitUntil(kj::mv(promise));
505+
}
492506
} else {
493507
lock.logWarningOnce("Received a QueueEvent but we lack a handler for QueueEvents. "
494508
"Did you remember to export a queue() function?");
495509
JSG_FAIL_REQUIRE(Error, "Handler does not export a queue() function.");
496510
}
497511
} else {
512+
isServiceWorkerHandler = true;
498513
if (globalEventTarget.getHandlerCount("queue") == 0) {
499514
lock.logWarningOnce("Received a QueueEvent but we lack an event listener for queue events. "
500515
"Did you remember to call addEventListener(\"queue\", ...)?");
@@ -504,8 +519,10 @@ jsg::Ref<QueueEvent> startQueueEvent(EventTarget& globalEventTarget,
504519
event->setCompletionStatus(QueueEvent::CompletedSuccessfully{});
505520
}
506521

507-
return event.addRef();
522+
return StartQueueEventResponse{
523+
kj::mv(event), kj::mv(exportedHandlerProm), isServiceWorkerHandler};
508524
}
525+
509526
} // namespace
510527

511528
kj::Promise<WorkerInterface::CustomEvent::Result> QueueCustomEventImpl::run(
@@ -515,6 +532,7 @@ kj::Promise<WorkerInterface::CustomEvent::Result> QueueCustomEventImpl::run(
515532
kj::TaskSet& waitUntilTasks) {
516533
incomingRequest->delivered();
517534
auto& context = incomingRequest->getContext();
535+
KJ_DEFER({ waitUntilTasks.add(incomingRequest->drain().attach(kj::mv(incomingRequest))); });
518536

519537
kj::String queueName;
520538
uint32_t batchSize;
@@ -542,6 +560,8 @@ kj::Promise<WorkerInterface::CustomEvent::Result> QueueCustomEventImpl::run(
542560
// waitUntil'ed callback safely without worrying about whether this coroutine gets canceled.
543561
struct QueueEventHolder: public kj::Refcounted {
544562
jsg::Ref<QueueEvent> event = nullptr;
563+
kj::Maybe<kj::Promise<void>> exportedHandlerProm;
564+
bool isServiceWorkerHandler = false;
545565
};
546566
auto queueEventHolder = kj::refcounted<QueueEventHolder>();
547567

@@ -555,16 +575,61 @@ kj::Promise<WorkerInterface::CustomEvent::Result> QueueCustomEventImpl::run(
555575
jsg::AsyncContextFrame::StorageScope traceScope = context.makeAsyncTraceScope(lock);
556576

557577
auto& typeHandler = lock.getWorker().getIsolate().getApi().getQueueTypeHandler(lock);
558-
queueEvent->event = startQueueEvent(lock.getGlobalScope(), kj::mv(params),
578+
auto startResp = startQueueEvent(lock.getGlobalScope(), kj::mv(params),
559579
context.addObject(result), lock,
560580
lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor()), typeHandler);
581+
queueEvent->event = kj::mv(startResp.event);
582+
queueEvent->exportedHandlerProm = kj::mv(startResp.exportedHandlerProm);
583+
queueEvent->isServiceWorkerHandler = startResp.isServiceWorkerHandler;
561584
});
562585

563586
auto compatFlags = context.getWorker().getIsolate().getApi().getFeatureFlags();
564587
if (compatFlags.getQueueConsumerNoWaitForWaitUntil()) {
565588
// The user has opted in to only waiting on their event handler rather than all waitUntil'd
566589
// promises.
567-
KJ_UNIMPLEMENTED("TODO(now)");
590+
auto timeoutPromise = context.getLimitEnforcer().limitScheduled();
591+
// Start invoking the queue handler. The promise chain here is intended to mimic the behavior of
592+
// finishScheduled, but only waiting on the promise returned by the event handler rather than on
593+
// all waitUntil'ed promises.
594+
auto outcome = co_await runProm
595+
.then([queueEvent = kj::addRef(
596+
*queueEventHolder)]() mutable -> kj::Promise<EventOutcome> {
597+
// If it returned a promise, wait on the promise.
598+
KJ_IF_SOME(handlerProm, queueEvent->exportedHandlerProm) {
599+
return handlerProm.then([]() { return EventOutcome::OK; });
600+
}
601+
return EventOutcome::OK;
602+
})
603+
.catch_([](kj::Exception&& e) {
604+
// If any exceptions were thrown, mark the outcome accordingly.
605+
return EventOutcome::EXCEPTION;
606+
})
607+
.exclusiveJoin(timeoutPromise.then([] {
608+
// Join everything against a timeout to ensure queue handlers can't run forever.
609+
return EventOutcome::EXCEEDED_CPU;
610+
})).exclusiveJoin(context.onAbort().then([] {
611+
// Also handle anything that might cause the worker to get aborted.
612+
// This is a change from the outcome we returned on abort before the compat flag, but better
613+
// matches the behavior of fetch() handlers and the semantics of what's actually happening.
614+
return EventOutcome::EXCEPTION;
615+
}, [](kj::Exception&&) { return EventOutcome::EXCEPTION; }));
616+
617+
if (outcome == EventOutcome::OK && queueEventHolder->isServiceWorkerHandler) {
618+
// HACK: For service-worker syntax, we effectively ignore the compatibility flag and wait
619+
// for all waitUntil tasks anyway, since otherwise there's no way to do async work from an
620+
// event listener callback.
621+
// It'd be nicer if we could fall through to the code below for the non-compat-flag logic in
622+
// this case, but we don't even know if the worker uses service worker syntax until after
623+
// runProm resolves, so we just copy the bare essentials here.
624+
auto result = co_await incomingRequest->finishScheduled();
625+
bool completed = result == IoContext_IncomingRequest::FinishScheduledResult::COMPLETED;
626+
outcome = completed ? context.waitUntilStatus() : EventOutcome::EXCEEDED_CPU;
627+
}
628+
629+
KJ_IF_SOME(status, context.getLimitEnforcer().getLimitsExceeded()) {
630+
outcome = status;
631+
}
632+
co_return WorkerInterface::CustomEvent::Result{.outcome = outcome};
568633
} else {
569634
// The user has not opted in to the new waitUntil behavior, so we need to add the queue()
570635
// handler's promise to the waitUntil promises and then wait on them all to finish.

src/workerd/io/compatibility-date.capnp

+2
Original file line numberDiff line numberDiff line change
@@ -712,4 +712,6 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef {
712712
# prevents a slow waitUntil'ed promise from slowing down consumption of messages from a queue,
713713
# which has been a recurring problem for the prior behavior (which did wait for all waitUntil'ed
714714
# tasks to complete.
715+
# This intentionally doesn't have a compatEnableDate yet until so we can let some users opt-in to
716+
# try it before enabling it for all new scripts, but will eventually need one.
715717
}

0 commit comments

Comments
 (0)