diff --git a/src/workerd/api/global-scope.h b/src/workerd/api/global-scope.h index 5a892ec2aad..3f59a4f53c9 100644 --- a/src/workerd/api/global-scope.h +++ b/src/workerd/api/global-scope.h @@ -17,6 +17,7 @@ #include #endif +#include #include #include @@ -193,11 +194,24 @@ class TestController: public jsg::Object { class ExecutionContext: public jsg::Object { public: ExecutionContext(jsg::Lock& js, jsg::JsValue exports) + : ExecutionContext(js, kj::mv(exports), /*props=*/js.obj(), /*versionInfo=*/kj::none) {} + + ExecutionContext(jsg::Lock& js, + jsg::JsValue exports, + jsg::JsValue props, + kj::Maybe versionInfo) : exports(js, exports), - props(js, js.obj()) {} - ExecutionContext(jsg::Lock& js, jsg::JsValue exports, jsg::JsValue props) - : exports(js, exports), - props(js, props) {} + props(js, props) { + auto featureFlags = FeatureFlags::get(js); + if (featureFlags.getEnableVersionApi()) { + version = kj::mv(versionInfo).map([&js, &featureFlags](auto v) { + return jsg::JsRef{js, + v.toJs(js, + featureFlags.getEnableCtxVersionMetadata() ? PopulateVersionInfoMetadata::YES + : PopulateVersionInfoMetadata::NO)}; + }); + } + } void waitUntil(kj::Promise promise); void passThroughOnException(); @@ -214,6 +228,17 @@ class ExecutionContext: public jsg::Object { return props.getHandle(js); } + jsg::JsValue getVersion(jsg::Lock& js) { + // TODO(soon): We should be able to assert for `version != kj::none` in the constructor when the + // `enable_version_api` compat flag is enabled, but currently dynamic workers and "reusable + // `ctx` object instantiation" do not pass information to populate `ctx.version` with, + // `ctx.version` is currently optional so we can return undefined for now. + KJ_IF_SOME(someVersion, version) { + return someVersion.getHandle(js); + } + return js.undefined(); + } + JSG_RESOURCE_TYPE(ExecutionContext, CompatibilityFlags::Reader flags) { JSG_METHOD(waitUntil); JSG_METHOD(passThroughOnException); @@ -221,6 +246,9 @@ class ExecutionContext: public jsg::Object { JSG_LAZY_INSTANCE_PROPERTY(exports, getExports); } JSG_LAZY_INSTANCE_PROPERTY(props, getProps); + if (flags.getEnableVersionApi()) { + JSG_LAZY_INSTANCE_PROPERTY(version, getVersion); + } if (flags.getWorkerdExperimental()) { // TODO(soon): Before making this generally available we need to: @@ -236,28 +264,57 @@ class ExecutionContext: public jsg::Object { JSG_METHOD(abort); } + // TODO(soon): This is getting unwieldy. if (flags.getEnableCtxExports()) { - JSG_TS_OVERRIDE( { - readonly props: Props; - readonly exports: Cloudflare.Exports; - }); + if (flags.getEnableVersionApi()) { + JSG_TS_OVERRIDE( { + readonly props: Props; + readonly exports: Cloudflare.Exports; + readonly version?: { + readonly metadata?: { readonly id: string; }; + readonly cohort?: string; + readonly key?: string; + readonly override?: string; + }; + }); + } else { + JSG_TS_OVERRIDE( { + readonly props: Props; + readonly exports: Cloudflare.Exports; + }); + } } else { - JSG_TS_OVERRIDE( { - readonly props: Props; - }); + if (flags.getEnableVersionApi()) { + JSG_TS_OVERRIDE( { + readonly props: Props; + readonly version?: { + readonly metadata?: { readonly id: string; }; + readonly cohort?: string; + readonly key?: string; + readonly override?: string; + }; + }); + } else { + JSG_TS_OVERRIDE( { + readonly props: Props; + }); + } } } void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { tracker.trackField("props", props); + tracker.trackField("version", version); } private: jsg::JsRef exports; jsg::JsRef props; + kj::Maybe> version; void visitForGc(jsg::GcVisitor& visitor) { visitor.visit(props); + visitor.visit(version); } }; diff --git a/src/workerd/api/hibernatable-web-socket.c++ b/src/workerd/api/hibernatable-web-socket.c++ index d6613a2ee7f..564f1493017 100644 --- a/src/workerd/api/hibernatable-web-socket.c++ +++ b/src/workerd/api/hibernatable-web-socket.c++ @@ -60,6 +60,7 @@ jsg::Ref HibernatableWebSocketEvent::claimWebSocket( kj::Promise HibernatableWebSocketCustomEvent::run( kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) { // Mark the request as delivered because we're about to run some JS. @@ -82,29 +83,33 @@ kj::Promise HibernatableWebSocketCustomEve try { co_await context.run( [entrypointName = entrypointName, &context, eventParameters = kj::mv(eventParameters), - props = kj::mv(props)](Worker::Lock& lock) mutable { + versionInfo = kj::mv(versionInfo), props = kj::mv(props)](Worker::Lock& lock) mutable { KJ_SWITCH_ONEOF(eventParameters.eventType) { KJ_CASE_ONEOF(text, HibernatableSocketParams::Text) { return lock.getGlobalScope().sendHibernatableWebSocketMessage(context, kj::mv(text.message), eventParameters.eventTimeoutMs, kj::mv(eventParameters.websocketId), lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor())); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor())); } KJ_CASE_ONEOF(data, HibernatableSocketParams::Data) { return lock.getGlobalScope().sendHibernatableWebSocketMessage(context, kj::mv(data.message), eventParameters.eventTimeoutMs, kj::mv(eventParameters.websocketId), lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor())); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor())); } KJ_CASE_ONEOF(close, HibernatableSocketParams::Close) { return lock.getGlobalScope().sendHibernatableWebSocketClose(context, kj::mv(close), eventParameters.eventTimeoutMs, kj::mv(eventParameters.websocketId), lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor())); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor())); } KJ_CASE_ONEOF(e, HibernatableSocketParams::Error) { return lock.getGlobalScope().sendHibernatableWebSocketError(context, kj::mv(e.error), eventParameters.eventTimeoutMs, kj::mv(eventParameters.websocketId), lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor())); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor())); } KJ_UNREACHABLE; } diff --git a/src/workerd/api/hibernatable-web-socket.h b/src/workerd/api/hibernatable-web-socket.h index 9f7f5b13b67..84da6c6d9db 100644 --- a/src/workerd/api/hibernatable-web-socket.h +++ b/src/workerd/api/hibernatable-web-socket.h @@ -66,6 +66,7 @@ class HibernatableWebSocketCustomEvent final: public WorkerInterface::CustomEven kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) override; diff --git a/src/workerd/api/queue.c++ b/src/workerd/api/queue.c++ index c3d6c611396..e329f6a27c4 100644 --- a/src/workerd/api/queue.c++ +++ b/src/workerd/api/queue.c++ @@ -573,6 +573,7 @@ tracing::EventInfo QueueCustomEvent::getEventInfo() const { kj::Promise QueueCustomEvent::run( kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) { // This method has three main chunks of logic: @@ -594,14 +595,16 @@ kj::Promise QueueCustomEvent::run( // 2. This is where we call into the worker's queue event handler auto runProm = context.run( [this, entrypointName = entrypointName, &context, queueEvent = kj::addRef(*queueEventHolder), - &metrics = incomingRequest->getMetrics(), + &metrics = incomingRequest->getMetrics(), versionInfo = kj::mv(versionInfo), props = kj::mv(props)](Worker::Lock& lock) mutable { jsg::AsyncContextFrame::StorageScope traceScope = context.makeAsyncTraceScope(lock); auto& typeHandler = lock.getWorker().getIsolate().getApi().getQueueTypeHandler(lock); auto startResp = startQueueEvent(lock.getGlobalScope(), context, kj::mv(params), context.addObject(result), lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor()), typeHandler); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor()), + typeHandler); queueEvent->event = kj::mv(startResp.event); queueEvent->exportedHandlerProm = kj::mv(startResp.exportedHandlerProm); queueEvent->isServiceWorkerHandler = startResp.isServiceWorkerHandler; diff --git a/src/workerd/api/queue.h b/src/workerd/api/queue.h index 23dd5913cb1..cd424871686 100644 --- a/src/workerd/api/queue.h +++ b/src/workerd/api/queue.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -341,6 +342,7 @@ class QueueCustomEvent final: public WorkerInterface::CustomEvent, public kj::Re kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) override; diff --git a/src/workerd/api/trace.c++ b/src/workerd/api/trace.c++ index 972e2cf9cdc..afd4eebe016 100644 --- a/src/workerd/api/trace.c++ +++ b/src/workerd/api/trace.c++ @@ -592,6 +592,7 @@ jsg::Ref UnsafeTraceMetrics::fromTrace(jsg::Lock& js, jsg::Ref sendTracesToExportedHandler(kj::Own incomingRequest, kj::Maybe entrypointNamePtr, + kj::Maybe versionInfo, Frankenvalue props, kj::ArrayPtr> traces) { // Mark the request as delivered because we're about to run some JS. @@ -614,10 +615,11 @@ kj::Promise sendTracesToExportedHandler(kj::Own incomingRequest, kj::Maybe entrypointNamePtr, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) -> kj::Promise { // Don't bother to wait around for the handler to run, just hand it off to the waitUntil tasks. waitUntilTasks.add(sendTracesToExportedHandler( - kj::mv(incomingRequest), entrypointNamePtr, kj::mv(props), traces)); + kj::mv(incomingRequest), entrypointNamePtr, kj::mv(versionInfo), kj::mv(props), traces)); // Reporting a proper outcome and return event here would be nice, but for that we'd need to await // running the tail handler... diff --git a/src/workerd/api/trace.h b/src/workerd/api/trace.h index 9cf9e6b50f0..ea3742854bd 100644 --- a/src/workerd/api/trace.h +++ b/src/workerd/api/trace.h @@ -618,6 +618,7 @@ class TraceCustomEvent final: public WorkerInterface::CustomEvent { kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) override; diff --git a/src/workerd/api/worker-rpc.c++ b/src/workerd/api/worker-rpc.c++ index 1b82db92909..a7c9d8f7475 100644 --- a/src/workerd/api/worker-rpc.c++ +++ b/src/workerd/api/worker-rpc.c++ @@ -1971,6 +1971,7 @@ class EntrypointJsRpcTarget final: public JsRpcTargetBase { public: EntrypointJsRpcTarget(IoContext& ioCtx, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::Maybe wrapperModule, kj::Maybe> tracer) @@ -1979,6 +1980,7 @@ class EntrypointJsRpcTarget final: public JsRpcTargetBase { // Most of the time we don't really have to clone this but it's hard to fully prove, so // let's be safe. entrypointName(entrypointName.map([](kj::StringPtr s) { return kj::str(s); })), + versionInfo(kj::mv(versionInfo)), props(kj::mv(props)), wrapperModule(kj::mv(wrapperModule)), tracer(kj::mv(tracer)) {} @@ -1997,9 +1999,9 @@ class EntrypointJsRpcTarget final: public JsRpcTargetBase { TargetInfo getTargetInfo(Worker::Lock& lock, IoContext& ioCtx) override { jsg::Lock& js = lock; - auto handler = - KJ_REQUIRE_NONNULL(lock.getExportedHandler(entrypointName, kj::mv(props), ioCtx.getActor()), - "Failed to get handler to worker."); + auto handler = KJ_REQUIRE_NONNULL(lock.getExportedHandler(entrypointName, kj::mv(versionInfo), + kj::mv(props), ioCtx.getActor()), + "Failed to get handler to worker."); if (handler->missingSuperclass && wrapperModule == kj::none) { // JS RPC is not enabled on the server side, we cannot call any methods. @@ -2063,6 +2065,7 @@ class EntrypointJsRpcTarget final: public JsRpcTargetBase { private: IoContext& ioCtx; kj::Maybe entrypointName; + kj::Maybe versionInfo; Frankenvalue props; kj::Maybe wrapperModule; kj::Maybe> tracer; @@ -2152,6 +2155,7 @@ class JsRpcSessionCustomEvent::ServerTopLevelMembrane final: public capnp::Membr kj::Promise JsRpcSessionCustomEvent::run( kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) { IoContext& ioctx = incomingRequest->getContext(); @@ -2164,8 +2168,8 @@ kj::Promise JsRpcSessionCustomEvent::run( waitUntilTasks.add(incomingRequest->drain().attach(kj::mv(incomingRequest))); }); - EntrypointJsRpcTarget target(ioctx, entrypointName, kj::mv(props), kj::mv(wrapperModule), - mapAddRef(incomingRequest->getWorkerTracer())); + EntrypointJsRpcTarget target(ioctx, entrypointName, kj::mv(versionInfo), kj::mv(props), + kj::mv(wrapperModule), mapAddRef(incomingRequest->getWorkerTracer())); capnp::RevocableServer revcableTarget(target); try { diff --git a/src/workerd/api/worker-rpc.h b/src/workerd/api/worker-rpc.h index b02a3095bce..afaa37cb2f9 100644 --- a/src/workerd/api/worker-rpc.h +++ b/src/workerd/api/worker-rpc.h @@ -468,6 +468,7 @@ class JsRpcSessionCustomEvent final: public WorkerInterface::CustomEvent { kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) override; diff --git a/src/workerd/io/compatibility-date.capnp b/src/workerd/io/compatibility-date.capnp index f31d42d0719..98dfaa3901c 100644 --- a/src/workerd/io/compatibility-date.capnp +++ b/src/workerd/io/compatibility-date.capnp @@ -1424,7 +1424,8 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef { $compatEnableFlag("enable_version_api") $experimental; # Enables version-related APIs. This currently only enables the `version` option in loopback - # bindings to specify a requested version. The behaviour of this flag will change in the future. + # bindings to specify a requested version and exposes `ctx.version`. The behaviour of this flag + # will change in the future. websocketBinaryTypeDefault @166 :Bool $compatEnableFlag("websocket_standard_binary_type") @@ -1494,4 +1495,11 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef { # WebSocket readyState is CLOSED (3) when the close event fires. Previously, # no close reply was sent and readyState was CLOSING (2). The WebSocket spec # requires readyState to be CLOSED when the close event is dispatched. + + enableCtxVersionMetadata @172 :Bool + $compatEnableFlag("enable_ctx_version_metadata") + $experimental; + # When paired with `enable_version_api`, also exposes `ctx.version.metadata`. This is a separate + # flag as we haven't decided on the exact behaviour of `ctx.version.metadata`, but the rest of + # `ctx.version` is much more well defined. The behaviour of this flag will change in the future. } diff --git a/src/workerd/io/trace-stream.c++ b/src/workerd/io/trace-stream.c++ index 5e6d6c98cdc..d743c3ac575 100644 --- a/src/workerd/io/trace-stream.c++ +++ b/src/workerd/io/trace-stream.c++ @@ -608,10 +608,12 @@ class TailStreamTarget final: public rpc::TailStreamTarget::Server { public: TailStreamTarget(IoContext& ioContext, kj::Maybe entrypointNamePtr, + kj::Maybe versionInfo, Frankenvalue props, kj::Own> doneFulfiller) : weakIoContext(ioContext.getWeakRef()), entrypointNamePtr(kj::mv(entrypointNamePtr)), + versionInfo(kj::mv(versionInfo)), props(kj::mv(props)), doneFulfiller(kj::mv(doneFulfiller)) {} @@ -723,8 +725,8 @@ class TailStreamTarget final: public rpc::TailStreamTarget::Server { events.size() == 1 && events[0].event.is(), "Expected only a single onset event"); auto& event = events[0]; - auto handler = KJ_REQUIRE_NONNULL( - lock.getExportedHandler(entrypointNamePtr, kj::mv(props), ioContext.getActor()), + auto handler = KJ_REQUIRE_NONNULL(lock.getExportedHandler(entrypointNamePtr, + kj::mv(versionInfo), kj::mv(props), ioContext.getActor()), "Failed to get handler to worker."); StringCache stringCache; @@ -916,6 +918,7 @@ class TailStreamTarget final: public rpc::TailStreamTarget::Server { kj::Own weakIoContext; kj::Maybe entrypointNamePtr; + kj::Maybe versionInfo; Frankenvalue props; // The done fulfiller is resolved when we receive the outcome event // or rejected if the capability is dropped before receiving the outcome @@ -939,14 +942,15 @@ EventInfo TailStreamCustomEvent::getEventInfo() const { kj::Promise TailStreamCustomEvent::run( kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) { IoContext& ioContext = incomingRequest->getContext(); incomingRequest->delivered(); auto [donePromise, doneFulfiller] = kj::newPromiseAndFulfiller(); - capFulfiller->fulfill(kj::heap( - ioContext, kj::mv(entrypointName), kj::mv(props), kj::mv(doneFulfiller))); + capFulfiller->fulfill(kj::heap(ioContext, kj::mv(entrypointName), + kj::mv(versionInfo), kj::mv(props), kj::mv(doneFulfiller))); donePromise = donePromise.attach(ioContext.registerPendingEvent()); diff --git a/src/workerd/io/trace-stream.h b/src/workerd/io/trace-stream.h index 740e9fba545..7d76c187dbf 100644 --- a/src/workerd/io/trace-stream.h +++ b/src/workerd/io/trace-stream.h @@ -28,6 +28,7 @@ class TailStreamCustomEvent final: public WorkerInterface::CustomEvent { kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) override; diff --git a/src/workerd/io/worker-entrypoint.c++ b/src/workerd/io/worker-entrypoint.c++ index 1114fd38850..0a9458d007e 100644 --- a/src/workerd/io/worker-entrypoint.c++ +++ b/src/workerd/io/worker-entrypoint.c++ @@ -59,6 +59,7 @@ class WorkerEntrypoint final: public WorkerInterface { bool tunnelExceptions, kj::Maybe> workerTracer, kj::Maybe cfBlobJson, + kj::Maybe versionInfo, kj::Maybe maybeTriggerInvocationSpan); kj::Promise request(kj::HttpMethod method, @@ -89,6 +90,7 @@ class WorkerEntrypoint final: public WorkerInterface { kj::Maybe entrypointName; Frankenvalue props; kj::Maybe cfBlobJson; + kj::Maybe versionInfo; // Hacky members used to hold some temporary state while processing a request. // See gory details in WorkerEntrypoint::request(). @@ -122,7 +124,8 @@ class WorkerEntrypoint final: public WorkerInterface { bool tunnelExceptions, kj::Maybe entrypointName, Frankenvalue props, - kj::Maybe cfBlobJson); + kj::Maybe cfBlobJson, + kj::Maybe versionInfo); }; // Simple wrapper around `HttpService::Response` to let us know if the response was sent @@ -175,11 +178,13 @@ kj::Own WorkerEntrypoint::construct(ThreadContext& threadContex bool tunnelExceptions, kj::Maybe> workerTracer, kj::Maybe cfBlobJson, + kj::Maybe versionInfo, kj::Maybe maybeTriggerInvocationSpan) { TRACE_EVENT("workerd", "WorkerEntrypoint::construct()"); - auto obj = kj::heap(kj::Badge(), threadContext, - waitUntilTasks, tunnelExceptions, entrypointName, kj::mv(props), kj::mv(cfBlobJson)); + auto obj = + kj::heap(kj::Badge(), threadContext, waitUntilTasks, + tunnelExceptions, entrypointName, kj::mv(props), kj::mv(cfBlobJson), kj::mv(versionInfo)); obj->init(kj::mv(worker), kj::mv(actor), kj::mv(limitEnforcer), kj::mv(ioContextDependency), kj::mv(ioChannelFactory), kj::addRef(*metrics), kj::mv(workerTracer), kj::mv(maybeTriggerInvocationSpan)); @@ -193,13 +198,15 @@ WorkerEntrypoint::WorkerEntrypoint(kj::Badge badge, bool tunnelExceptions, kj::Maybe entrypointName, Frankenvalue props, - kj::Maybe cfBlobJson) + kj::Maybe cfBlobJson, + kj::Maybe versionInfo) : threadContext(threadContext), waitUntilTasks(waitUntilTasks), tunnelExceptions(tunnelExceptions), entrypointName(entrypointName), props(kj::mv(props)), - cfBlobJson(kj::mv(cfBlobJson)) {} + cfBlobJson(kj::mv(cfBlobJson)), + versionInfo(kj::mv(versionInfo)) {} void WorkerEntrypoint::init(kj::Own worker, kj::Maybe> actor, @@ -321,7 +328,9 @@ kj::Promise WorkerEntrypoint::request(kj::HttpMethod method, return lock.getGlobalScope().request(method, url, headers, requestBody, wrappedResponse, cfBlobJson, lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor()), kj::mv(signal)); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor()), + kj::mv(signal)); }) .then([this, &context, &wrappedResponse = *wrappedResponse, workerTracer]( api::DeferredProxy deferredProxy) { @@ -598,14 +607,16 @@ kj::Promise WorkerEntrypoint::runScheduled( incomingRequest->delivered(); // Scheduled handlers run entirely in waitUntil() tasks. - context.addWaitUntil(context.run( - [scheduledTime, cron, entrypointName = entrypointName, props = kj::mv(props), &context, - &metrics = incomingRequest->getMetrics()](Worker::Lock& lock) mutable { + context.addWaitUntil( + context.run([scheduledTime, cron, entrypointName = entrypointName, + versionInfo = kj::mv(versionInfo), props = kj::mv(props), &context, + &metrics = incomingRequest->getMetrics()](Worker::Lock& lock) mutable { TRACE_EVENT("workerd", "WorkerEntrypoint::runScheduled() run"); jsg::AsyncContextFrame::StorageScope traceScope = context.makeAsyncTraceScope(lock); lock.getGlobalScope().startScheduled(scheduledTime, cron, lock, - lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor())); + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor())); })); static auto constexpr waitForFinished = [](IoContext& context, @@ -676,7 +687,8 @@ kj::Promise WorkerEntrypoint::runAlarmImpl( try { auto result = co_await context.run([scheduledTime, retryCount, entrypointName = entrypointName, - props = kj::mv(props), &context](Worker::Lock& lock) mutable { + versionInfo = kj::mv(versionInfo), props = kj::mv(props), + &context](Worker::Lock& lock) mutable { jsg::AsyncContextFrame::StorageScope traceScope = context.makeAsyncTraceScope(lock); // If we have an invalid timeout, set it to the default value of 15 minutes. @@ -686,7 +698,8 @@ kj::Promise WorkerEntrypoint::runAlarmImpl( timeout = 15 * kj::MINUTES; } - auto handler = lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor()); + auto handler = lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor()); return lock.getGlobalScope().runAlarm(scheduledTime, timeout, retryCount, lock, handler); }); @@ -752,15 +765,17 @@ kj::Promise WorkerEntrypoint::test() { incomingRequest->delivered(); - context.addWaitUntil(context.run([entrypointName = entrypointName, props = kj::mv(props), - &context, &metrics = incomingRequest->getMetrics()]( - Worker::Lock& lock) mutable -> kj::Promise { + context.addWaitUntil( + context.run([entrypointName = entrypointName, versionInfo = kj::mv(versionInfo), + props = kj::mv(props), &context, &metrics = incomingRequest->getMetrics()]( + Worker::Lock& lock) mutable -> kj::Promise { TRACE_EVENT("workerd", "WorkerEntrypoint::test() run"); jsg::AsyncContextFrame::StorageScope traceScope = context.makeAsyncTraceScope(lock); return context.awaitJs(lock, - lock.getGlobalScope().test( - lock, lock.getExportedHandler(entrypointName, kj::mv(props), context.getActor()))); + lock.getGlobalScope().test(lock, + lock.getExportedHandler( + entrypointName, kj::mv(versionInfo), kj::mv(props), context.getActor()))); })); static auto constexpr waitForFinished = @@ -810,7 +825,9 @@ kj::Promise WorkerEntrypoint::customEvent( t.setEventInfo(*incomingRequest, event->getEventInfo()); } - auto promise = event->run(kj::mv(incomingRequest), entrypointName, kj::mv(props), waitUntilTasks) + auto promise = event + ->run(kj::mv(incomingRequest), entrypointName, kj::mv(versionInfo), + kj::mv(props), waitUntilTasks) .attach(kj::mv(event)); // TODO(cleanup): In theory `context` may have been destroyed by now if `event->run()` dropped @@ -871,11 +888,13 @@ kj::Own newWorkerEntrypoint(ThreadContext& threadContext, bool tunnelExceptions, kj::Maybe> workerTracer, kj::Maybe cfBlobJson, + kj::Maybe versionInfo, kj::Maybe maybeTriggerInvocationSpan) { return WorkerEntrypoint::construct(threadContext, kj::mv(worker), kj::mv(entrypointName), kj::mv(props), kj::mv(actor), kj::mv(limitEnforcer), kj::mv(ioContextDependency), kj::mv(ioChannelFactory), kj::mv(metrics), waitUntilTasks, tunnelExceptions, - kj::mv(workerTracer), kj::mv(cfBlobJson), kj::mv(maybeTriggerInvocationSpan)); + kj::mv(workerTracer), kj::mv(cfBlobJson), kj::mv(versionInfo), + kj::mv(maybeTriggerInvocationSpan)); } } // namespace workerd diff --git a/src/workerd/io/worker-entrypoint.h b/src/workerd/io/worker-entrypoint.h index 6ed7ae38c5e..7bf41b1a06f 100644 --- a/src/workerd/io/worker-entrypoint.h +++ b/src/workerd/io/worker-entrypoint.h @@ -41,6 +41,7 @@ kj::Own newWorkerEntrypoint(ThreadContext& threadContext, bool tunnelExceptions, kj::Maybe> workerTracer, kj::Maybe cfBlobJson, + kj::Maybe versionInfo, // The trigger invocation span may be propagated from other request. If it is provided, // the implication is that this worker entrypoint is being created as a subrequest or // subtask of another request. If it is kj::none, then this invocation is a top-level diff --git a/src/workerd/io/worker-interface.h b/src/workerd/io/worker-interface.h index f68085d9382..43c66714c2a 100644 --- a/src/workerd/io/worker-interface.h +++ b/src/workerd/io/worker-interface.h @@ -17,6 +17,7 @@ namespace workerd { class Frankenvalue; class IoContext_IncomingRequest; +struct Worker_VersionInfo; // An interface representing the services made available by a worker/pipeline to handle a // request. @@ -115,6 +116,7 @@ class WorkerInterface: public kj::HttpService { // for this event. virtual kj::Promise run(kj::Own incomingRequest, kj::Maybe entrypointName, + kj::Maybe versionInfo, Frankenvalue props, kj::TaskSet& waitUntilTasks) = 0; diff --git a/src/workerd/io/worker.c++ b/src/workerd/io/worker.c++ index 143c597ec72..b343711b43e 100644 --- a/src/workerd/io/worker.c++ +++ b/src/workerd/io/worker.c++ @@ -2309,7 +2309,10 @@ static inline kj::Own fakeOwn(T& ref) { } kj::Maybe> Worker::Lock::getExportedHandler( - kj::Maybe name, Frankenvalue props, kj::Maybe actor) { + kj::Maybe name, + kj::Maybe versionInfo, + Frankenvalue props, + kj::Maybe actor) { KJ_IF_SOME(a, actor) { KJ_IF_SOME(h, a.getHandler()) { return fakeOwn(h); @@ -2323,7 +2326,8 @@ kj::Maybe> Worker::Lock::getExportedHandler( jsg::Lock& js = *this; auto handler = kj::heap(cls(js, js.alloc(js, - jsg::JsValue(KJ_ASSERT_NONNULL(worker.impl->ctxExports).getHandle(js)), props.toJs(js)), + jsg::JsValue(KJ_ASSERT_NONNULL(worker.impl->ctxExports).getHandle(js)), props.toJs(js), + kj::mv(versionInfo)), KJ_ASSERT_NONNULL(worker.impl->env).addRef(js))); // HACK: We set handler.env and handler.ctx to undefined because we already passed the real @@ -2340,7 +2344,8 @@ kj::Maybe> Worker::Lock::getExportedHandler( if (!FeatureFlags::get(js).getReuseCtxAcrossNonclassEvents()) { api::ExportedHandler constructedHandler = h.clone(js); constructedHandler.ctx = js.alloc(js, - jsg::JsValue(KJ_ASSERT_NONNULL(worker.impl->ctxExports).getHandle(js)), props.toJs(js)); + jsg::JsValue(KJ_ASSERT_NONNULL(worker.impl->ctxExports).getHandle(js)), props.toJs(js), + kj::mv(versionInfo)); return kj::heap(kj::mv(constructedHandler)); } return fakeOwn(h); diff --git a/src/workerd/io/worker.h b/src/workerd/io/worker.h index fac5362a67f..05f180b901f 100644 --- a/src/workerd/io/worker.h +++ b/src/workerd/io/worker.h @@ -95,6 +95,7 @@ struct EntrypointClasses { // for a class name to end in "Instance". ("I have an instance of WorkerInstance...") class Worker: public kj::AtomicRefcounted { public: + using VersionInfo = Worker_VersionInfo; class Script; class Isolate; class Api; @@ -752,12 +753,16 @@ class Worker::Lock { // default handler. Returns null if this is not a modules-syntax worker (but `entrypointName` // must be null in that case). // + // `versionInfo` is used to populate `ctx.version` when enabled. // `props` is the value to place in `ctx.props`. // // If running in an actor, the name and props are ignored and the entrypoint originally used to // construct the actor is returned. kj::Maybe> getExportedHandler( - kj::Maybe entrypointName, Frankenvalue props, kj::Maybe actor); + kj::Maybe entrypointName, + kj::Maybe versionInfo, + Frankenvalue props, + kj::Maybe actor); // Get the C++ object representing the global scope. api::ServiceWorkerGlobalScope& getGlobalScope(); @@ -1028,6 +1033,47 @@ class Worker::Actor final: public kj::Refcounted { kj::Promise ensureConstructedImpl(IoContext&, ActorClassInfo& info); }; +WD_STRONG_BOOL(PopulateVersionInfoMetadata); + +// Version information associated with a worker. These are made available through `ctx.version`. +// This is also available through the preferred `Worker::VersionInfo` alias. `Worker_VersionInfo` +// exists to allow for forward declaration in a couple of niche places. +struct Worker_VersionInfo { + kj::String id; + kj::Maybe cohort; + kj::Maybe key; + kj::Maybe versionOverride; + + Worker_VersionInfo clone() const { + return { + .id = kj::str(id), + .cohort = cohort.map([](const kj::String& s) { return kj::str(s); }), + .key = key.map([](const kj::String& s) { return kj::str(s); }), + .versionOverride = versionOverride.map([](const kj::String& s) { return kj::str(s); }), + }; + } + + jsg::JsValue toJs(jsg::Lock& js, PopulateVersionInfoMetadata populateVersionInfoMetadata) const { + auto version = js.obj(); + if (populateVersionInfoMetadata) { + auto metadata = js.obj(); + metadata.set(js, "id"_kj, js.str(id)); + version.set(js, "metadata"_kj, metadata); + } + KJ_IF_SOME(someCohort, cohort) { + version.set(js, "cohort"_kj, js.str(someCohort)); + } + KJ_IF_SOME(someKey, key) { + version.set(js, "key"_kj, js.str(someKey)); + } + KJ_IF_SOME(someVersionOverride, versionOverride) { + version.set(js, "override"_kj, js.str(someVersionOverride)); + } + version.recursivelyFreeze(js); + return version; + } +}; + // ======================================================================================= // inline implementation details diff --git a/src/workerd/server/server.c++ b/src/workerd/server/server.c++ index e4d13c96403..7bb9429afc5 100644 --- a/src/workerd/server/server.c++ +++ b/src/workerd/server/server.c++ @@ -2188,14 +2188,16 @@ class Server::WorkerService final: public Service, kj::Own observer = kj::refcounted(mapAddRef(workerTracer), waitUntilTasks); - return newWorkerEntrypoint(threadContext, kj::atomicAddRef(*worker), entrypointName, - kj::mv(props), kj::mv(actor), kj::Own(this, kj::NullDisposer::instance), - {}, // ioContextDependency + return newWorkerEntrypoint( + threadContext, kj::atomicAddRef(*worker), entrypointName, kj::mv(props), kj::mv(actor), + kj::Own(this, kj::NullDisposer::instance), {}, // ioContextDependency kj::Own(this, kj::NullDisposer::instance), kj::mv(observer), waitUntilTasks, true, // tunnelExceptions kj::mv(workerTracer), // workerTracer - kj::mv(metadata.cfBlobJson)); + kj::mv(metadata.cfBlobJson), + kj::none // versionInfo + ); } class ActorNamespace final { diff --git a/src/workerd/tests/test-fixture.c++ b/src/workerd/tests/test-fixture.c++ index 7e7fafe4cb0..9b2586ebb27 100644 --- a/src/workerd/tests/test-fixture.c++ +++ b/src/workerd/tests/test-fixture.c++ @@ -478,7 +478,10 @@ TestFixture::Response TestFixture::runRequest( runInIoContext([&](const TestFixture::Environment& env) { auto& globalScope = env.lock.getGlobalScope(); return globalScope.request(method, url, requestHeaders, *requestBody, response, "{}"_kj, - env.lock, env.lock.getExportedHandler(kj::none, {}, kj::none), /* abortSignal */ kj::none); + env.lock, + env.lock.getExportedHandler(/*entryPointName=*/kj::none, /*versionInfo=*/kj::none, + /*props=*/{}, /*actor=*/kj::none), + /*abortSignal=*/kj::none); }); return {.statusCode = response.statusCode, .body = response.body->str()}; diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts index 5eaf7c6d33f..cccaf66ea6a 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -492,6 +492,14 @@ interface ExecutionContext { passThroughOnException(): void; readonly exports: Cloudflare.Exports; readonly props: Props; + readonly version?: { + readonly metadata?: { + readonly id: string; + }; + readonly cohort?: string; + readonly key?: string; + readonly override?: string; + }; abort(reason?: any): void; } type ExportedHandlerFetchHandler< diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts index dfe677e59d5..0d146f69917 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -494,6 +494,14 @@ export interface ExecutionContext { passThroughOnException(): void; readonly exports: Cloudflare.Exports; readonly props: Props; + readonly version?: { + readonly metadata?: { + readonly id: string; + }; + readonly cohort?: string; + readonly key?: string; + readonly override?: string; + }; abort(reason?: any): void; } export type ExportedHandlerFetchHandler<