Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/workerd/api/unsafe.c++
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "unsafe.h"

#include <workerd/api/http.h>
#include <workerd/io/io-context.h>
#include <workerd/jsg/jsg.h>
#include <workerd/jsg/script.h>
Expand All @@ -18,6 +19,21 @@ static constexpr auto ASYNC_FN_SUFFIX = "}"_kjc;
inline kj::StringPtr getName(jsg::Optional<kj::String>& name, kj::StringPtr def) {
return name.map([](kj::String& str) { return str.asPtr(); }).orDefault(def);
}

IoChannelFactory::EvictWebSocketMode parseEvictWebSocketMode(
jsg::Optional<UnsafeModule::EvictOptions> options) {
auto opts = kj::mv(options).orDefault({});
KJ_IF_SOME(webSockets, opts.webSockets) {
if (webSockets == "hibernate") {
return IoChannelFactory::EvictWebSocketMode::HIBERNATE;
}
if (webSockets == "close") {
return IoChannelFactory::EvictWebSocketMode::CLOSE;
}
JSG_FAIL_REQUIRE(TypeError, "options.webSockets must be \"hibernate\" or \"close\".");
}
return IoChannelFactory::EvictWebSocketMode::HIBERNATE;
}
} // namespace

#ifdef WORKERD_FUZZILLI
Expand Down Expand Up @@ -213,6 +229,31 @@ jsg::Promise<void> UnsafeModule::deleteAllDurableObjects(jsg::Lock& js) {
return js.resolvedPromise();
}

jsg::Promise<void> UnsafeModule::evict(
jsg::Lock& js, jsg::Ref<Fetcher> stub, jsg::Optional<EvictOptions> options) {
auto& context = IoContext::current();

// Resolve the stub to its underlying channel. For a Durable Object stub this is an actor
// channel that supports evictForTest(); for any other Fetcher, evictForTest() throws.
// Use kj::evalNow() so that a synchronous throw (e.g. "not a Durable Object stub" or "not
// currently running") becomes a rejected promise rather than a synchronous exception.
auto channel = stub->getSubrequestChannel(context);
auto promise = kj::evalNow([&channel, options = kj::mv(options)]() mutable {
return channel->evictForTest(parseEvictWebSocketMode(kj::mv(options)));
}).attach(kj::mv(channel));
return context.awaitIo(js, kj::mv(promise));
}

jsg::Promise<void> UnsafeModule::evictAllDurableObjects(
jsg::Lock& js, jsg::Optional<EvictOptions> options) {
auto& context = IoContext::current();
// Use kj::evalNow() so that a synchronous throw becomes a rejected promise rather than a
// synchronous exception, matching evict()'s behaviour.
return context.awaitIo(js, kj::evalNow([&context, options = kj::mv(options)]() mutable {
return context.evictAllActorsForTest(parseEvictWebSocketMode(kj::mv(options)));
}));
}

bool UnsafeModule::isTestAutogateEnabled() {
return util::Autogate::isEnabled(util::AutogateKey::TEST_WORKERD);
}
Expand Down
33 changes: 31 additions & 2 deletions src/workerd/api/unsafe.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

namespace workerd::api {

class Fetcher;

// A special binding object that allows for dynamic evaluation.
class UnsafeEval: public jsg::Object {
public:
Expand Down Expand Up @@ -104,13 +106,38 @@ class UnsafeModule: public jsg::Object {
// restart with clean state. Namespaces with preventEviction are not affected.
jsg::Promise<void> deleteAllDurableObjects(jsg::Lock& js);

struct EvictOptions {
jsg::Optional<kj::String> webSockets;

JSG_STRUCT(webSockets);
JSG_STRUCT_TS_OVERRIDE({
webSockets?: "hibernate" | "close";
});
};

// Test-only: gracefully evict the Durable Object referred to by `stub` from its isolate,
// simulating the runtime tearing it down when it goes idle. Durable storage is left intact, so
// the DO rebuilds (rerunning its constructor) on its next request. Hibernatable WebSockets are
// hibernated by default, or closed if options.webSockets is "close". Rejects if `stub` is not a
// Durable Object stub, or if the target DO is not currently running.
jsg::Promise<void> evict(
jsg::Lock& js, jsg::Ref<Fetcher> stub, jsg::Optional<EvictOptions> options);

// Test-only: gracefully evict every currently-running Durable Object that this worker can
// address (in evictable namespaces). Unlike abortAllDurableObjects(), this preserves durable
// storage and hibernates hibernatable WebSockets by default, or closes them if options.webSockets
// is "close". Idle DOs are skipped (not an error).
jsg::Promise<void> evictAllDurableObjects(jsg::Lock& js, jsg::Optional<EvictOptions> options);

// Returns true if the TEST_WORKERD autogate is enabled.
// This is used to verify that the all-autogates test variant is working correctly.
bool isTestAutogateEnabled();

JSG_RESOURCE_TYPE(UnsafeModule) {
JSG_METHOD(abortAllDurableObjects);
JSG_METHOD(deleteAllDurableObjects);
JSG_METHOD(evict);
JSG_METHOD(evictAllDurableObjects);
JSG_METHOD(isTestAutogateEnabled);
}
};
Expand Down Expand Up @@ -142,9 +169,11 @@ void registerUnsafeModule(Registry& registry) {
}

#ifdef WORKERD_FUZZILLI
#define EW_UNSAFE_ISOLATE_TYPES api::UnsafeEval, api::UnsafeModule, api::Stdin, api::Fuzzilli
#define EW_UNSAFE_ISOLATE_TYPES \
api::UnsafeEval, api::UnsafeModule, api::UnsafeModule::EvictOptions, api::Stdin, api::Fuzzilli
#else
#define EW_UNSAFE_ISOLATE_TYPES api::UnsafeEval, api::UnsafeModule, api::Stdin
#define EW_UNSAFE_ISOLATE_TYPES \
api::UnsafeEval, api::UnsafeModule, api::UnsafeModule::EvictOptions, api::Stdin
#endif

template <class Registry>
Expand Down
26 changes: 26 additions & 0 deletions src/workerd/io/io-channels.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ struct DynamicWorkerSource;
// anything in the world except for the client -- this is a useful property for sandboxing!
class IoChannelFactory {
public:
enum class EvictWebSocketMode {
HIBERNATE,
CLOSE,
};

// Opaque, IoContext-independent handle that knows how to construct a channel token referring to
// the current entrypoint ("self"). Used to implement `ctx.restore()`: the implementation of
// `ctx.restore()` passes this back into `makeRestored*()` so that the resulting restored channel
Expand Down Expand Up @@ -258,6 +263,18 @@ class IoChannelFactory {
// Note that the caller is expected to keep the SubrequestChannel alive until it is done with
// the returned WorkerInterface.
virtual kj::Own<WorkerInterface> startRequest(SubrequestMetadata metadata) = 0;

// Test-only: forcibly evict the target of this channel from its isolate, simulating the
// runtime tearing it down when it goes idle. For a Durable Object stub this destroys the actor
// instance while durable storage survives, so the next request rebuilds it. Depending on
// webSocketMode, hibernatable WebSockets are either hibernated first or closed. Only channels
// that point at an actor support this; others throw.
//
// Throws if the target Durable Object is not currently running (never instantiated, or
// already evicted/hibernated).
virtual kj::Promise<void> evictForTest(EvictWebSocketMode webSocketMode) {
JSG_FAIL_REQUIRE(Error, "evict() can only be used on a Durable Object stub.");
}
};

// Obtain an object representing a particular subrequest channel.
Expand Down Expand Up @@ -362,6 +379,15 @@ class IoChannelFactory {
KJ_UNIMPLEMENTED("Only implemented by single-tenant workerd runtime");
}

// Test-only: gracefully evict every currently-running actor in the evictable namespaces this
// worker can address, simulating idle teardown. Unlike abortAllActors(), this leaves durable
// storage intact, so DOs rebuild on their next request. Depending on webSocketMode, hibernatable
// WebSockets are either hibernated first or closed. Actors that aren't currently running are
// skipped (no error).
virtual kj::Promise<void> evictAllActorsForTest(EvictWebSocketMode webSocketMode) {
KJ_UNIMPLEMENTED("Only implemented by single-tenant workerd runtime");
}

// In workerd, the handler aborts the process (unless used on a dynamic
// worker). In the edge runtime it will condemn and terminate the current
// isolate.
Expand Down
6 changes: 6 additions & 0 deletions src/workerd/io/io-context.h
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,12 @@ class IoContext final: public kj::Refcounted, private kj::TaskSet::ErrorHandler
getIoChannelFactory().deleteAllActors(reason);
}

// Test-only: gracefully evict all currently-running actors. See
// IoChannelFactory::evictAllActorsForTest().
kj::Promise<void> evictAllActorsForTest(IoChannelFactory::EvictWebSocketMode webSocketMode) {
return getIoChannelFactory().evictAllActorsForTest(webSocketMode);
}

// Condemn and terminate JS isolate
void abortIsolate(kj::StringPtr reason = nullptr);

Expand Down
4 changes: 4 additions & 0 deletions src/workerd/io/request-tracker.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class RequestTracker final: public kj::Refcounted {
return kj::addRef(*this);
}

bool isActive() const {
return activeRequests > 0;
}

private:
void requestActive();
void requestInactive();
Expand Down
Loading
Loading