Skip to content

Commit

Permalink
Add config to prevent Durable Object eviction
Browse files Browse the repository at this point in the history
Miniflare depends on Durable Objects staying in memory forever.
This commit provides a way to ensure a DO namespace cannot be evicted
(unless it is broken), thereby retaining the old behavior.
  • Loading branch information
MellowYarker committed Sep 27, 2023
1 parent 99de92f commit 8c924b3
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 12 deletions.
77 changes: 77 additions & 0 deletions src/workerd/server/server-test.c++
Original file line number Diff line number Diff line change
Expand Up @@ -1790,6 +1790,83 @@ KJ_TEST("Server: Durable Objects (ephemeral) eviction") {
connTwo.httpGet200("/checkEvicted", "OK");
}

KJ_TEST("Server: Durable Objects (ephemeral) prevent eviction") {
TestServer test(R"((
services = [
( name = "hello",
worker = (
compatibilityDate = "2023-08-17",
modules = [
( name = "main.js",
esModule =
`export default {
` async fetch(request, env) {
` let id = env.ns.idFromName("59002eb8cf872e541722977a258a12d6a93bbe8192b502e1c0cb250aa91af234");
` let obj = env.ns.get(id);
` if (request.url.endsWith("/setup")) {
` return await obj.fetch("http://example.com/setup");
` } else if (request.url.endsWith("/assertNotEvicted")) {
` try {
` return await obj.fetch("http://example.com/assertNotEvicted");
` } catch(e) {
` throw e;
` }
` }
` return new Response("Invalid Route!")
` }
`}
`export class MyActorClass {
` constructor(state, env) {
` this.defaultMessage = false; // Set to true on first "setup" request
` }
` async fetch(request) {
` if (request.url.endsWith("/setup")) {
` // Request 1, set defaultMessage, will remain true as long as actor is live.
` this.defaultMessage = true;
` return new Response("OK");
` } else if (request.url.endsWith("/assertNotEvicted")) {
` // Request 2, assert that actor is still in alive (defaultMessage is still true).
` if (this.defaultMessage) {
` // Actor is still alive and we did not re-run the constructor
` return new Response("OK");
` }
` throw new Error("Error: Actor was evicted!");
` }
` }
`}
)
],
bindings = [(name = "ns", durableObjectNamespace = "MyActorClass")],
durableObjectNamespaces = [
( className = "MyActorClass",
uniqueKey = "mykey",
preventEviction = true,
)
],
durableObjectStorage = (inMemory = void)
)
),
],
sockets = [
( name = "main",
address = "test-addr",
service = "hello"
)
]
))"_kj);

test.start();
auto conn = test.connect("test-addr");
conn.httpGet200("/setup", "OK");
conn.httpGet200("/assertNotEvicted", "OK");

// Attempt to force hibernation by waiting 10 seconds.
test.wait(10);
// Need a second connection because of 5 second HTTP timeout.
auto connTwo = test.connect("test-addr");
connTwo.httpGet200("/assertNotEvicted", "OK");
}

KJ_TEST("Server: Durable Object evictions when callback scheduled") {
kj::StringPtr config = R"((
services = [
Expand Down
46 changes: 36 additions & 10 deletions src/workerd/server/server.c++
Original file line number Diff line number Diff line change
Expand Up @@ -1426,14 +1426,26 @@ public:
}

void inactive() override {
KJ_IF_SOME(a, actor) {
KJ_IF_SOME(m, a->getHibernationManager()) {
// The hibernation manager needs to survive actor eviction and be passed to the actor
// constructor next time we create it.
manager = kj::addRef(*static_cast<HibernationManagerImpl*>(&m));
// Durable objects are evictable by default.
bool isEvictable = true;
KJ_SWITCH_ONEOF(parent.config) {
KJ_CASE_ONEOF(c, Durable) {
isEvictable = c.isEvictable;
}
KJ_CASE_ONEOF(c, Ephemeral) {
isEvictable = c.isEvictable;
}
}
if (isEvictable) {
KJ_IF_SOME(a, actor) {
KJ_IF_SOME(m, a->getHibernationManager()) {
// The hibernation manager needs to survive actor eviction and be passed to the actor
// constructor next time we create it.
manager = kj::addRef(*static_cast<HibernationManagerImpl*>(&m));
}
}
shutdownTask = handleShutdown().eagerlyEvaluate([](kj::Exception&& e) { KJ_LOG(ERROR, e); });
}
shutdownTask = handleShutdown().eagerlyEvaluate([](kj::Exception&& e) { KJ_LOG(ERROR, e); });
}

// Processes the eviction of the Durable Object and hibernates active websockets.
Expand Down Expand Up @@ -1575,8 +1587,19 @@ public:
while (true) {
auto now = timer.now();
actors.eraseAll([&](auto&, kj::Own<ActorContainer>& entry) {
if (entry->hasClients()) {
// We are still using the actor so we cannot remove it!

// Durable Objects are evictable by default.
bool isEvictable = true;
KJ_SWITCH_ONEOF(config) {
KJ_CASE_ONEOF(c, Durable) {
isEvictable = c.isEvictable;
}
KJ_CASE_ONEOF(c, Ephemeral) {
isEvictable = c.isEvictable;
}
}
if (entry->hasClients() || !isEvictable) {
// We are still using the actor so we cannot remove it, or this actor cannot be evicted.
return false;
}

Expand Down Expand Up @@ -2881,7 +2904,9 @@ void Server::startServices(jsg::V8System& v8System, config::Config::Reader confi
case config::Worker::DurableObjectNamespace::UNIQUE_KEY:
hadDurable = true;
serviceActorConfigs.insert(kj::str(ns.getClassName()),
Durable { kj::str(ns.getUniqueKey()) });
Durable {
.uniqueKey = kj::str(ns.getUniqueKey()),
.isEvictable = !ns.getPreventEviction() });
continue;
case config::Worker::DurableObjectNamespace::EPHEMERAL_LOCAL:
if (!experimental) {
Expand All @@ -2890,7 +2915,8 @@ void Server::startServices(jsg::V8System& v8System, config::Config::Reader confi
"experimental feature which may change or go away in the future. You must run "
"workerd with `--experimental` to use this feature."));
}
serviceActorConfigs.insert(kj::str(ns.getClassName()), Ephemeral {});
serviceActorConfigs.insert(kj::str(ns.getClassName()),
Ephemeral { .isEvictable = !ns.getPreventEviction() });
continue;
}
reportConfigError(kj::str(
Expand Down
9 changes: 7 additions & 2 deletions src/workerd/server/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,13 @@ class Server: private kj::TaskSet::ErrorHandler {
kj::StringPtr servicePattern = "*"_kj,
kj::StringPtr entrypointPattern = "*"_kj);

struct Durable { kj::String uniqueKey; };
struct Ephemeral {};
struct Durable {
kj::String uniqueKey;
bool isEvictable;
};
struct Ephemeral {
bool isEvictable;
};
using ActorConfig = kj::OneOf<Durable, Ephemeral>;

class InspectorService;
Expand Down
7 changes: 7 additions & 0 deletions src/workerd/server/workerd.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,13 @@ struct Worker {
# anything. An object that hasn't stored anything will not consume any storage space on
# disk.
}

preventEviction @3 :Bool;
# By default, Durable Objects are evicted after 10 seconds of inactivity, and expire 70 seconds
# after all clients have disconnected. Some applications may want to keep their Durable Objects
# pinned to memory forever, so we provide this flag to change the default behavior.
#
# Note that this is only supported in Workerd; production Durable Objects cannot toggle eviction.
}

durableObjectUniqueKeyModifier @8 :Text;
Expand Down

0 comments on commit 8c924b3

Please sign in to comment.