Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Durable Objects (in-memory only, for testing) #15

Merged
merged 5 commits into from Sep 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -42,7 +42,7 @@ As of this writing, some major features are missing which we intend to fix short
* **Wrangler/Miniflare integration** is in progress. The [Wrangler CLI tool](https://developers.cloudflare.com/workers/wrangler/) and [Miniflare](https://miniflare.dev/) will soon support local testing using `workerd` (replacing the previous simulated environment on top of Node). Wrangler should also support generating `workerd` configuration directly from a Wrangler project.
* **Multi-threading** is not implemented. `workerd` runs in a single-threaded event loop. For now, to utilize multiple cores, we suggest running multiple instances of `workerd` and balancing load across them. We will likely add some built-in functionality for this in the near future.
* **Performance tuning** has not been done yet, and there is low-hanging fruit here. `workerd` performs decently as-is, but not spectacularly. Experiments suggest we can roughly double performance on a "hello world" load test with some tuning of compiler optimization flags and memory allocators.
* **Durable Objects** are not supported yet. We will add support for in-memory Durable Objects shortly, to allow for local testing of DO-based applications. Durable Objects that are actually durable, or distributed across multiple machines, are a longer-term project. Cloudflare's internal implementation of this is heavily tied to the specifics of Cloudflare's network, so a new implementation needs to be developed for public consumption.
* **Durable Objects** are currently supported only in a mode that uses in-memory storage -- i.e., not actually "durable". This is useful for local testing of DO-based apps, but not for production. Durable Objects that are actually durable, or distributed across multiple machines, are a longer-term project. Cloudflare's internal implementation of this is heavily tied to the specifics of Cloudflare's network, so a new implementation needs to be developed for public consumption.
* **Cache API** emulation is not implemented yet.
* **Cron trigger** emulation is not supported yet. We need to figure out how, exactly, this should work in the first place. Typically if you have a cluster of machines, you only want a cron event to run on one of the machines, so some sort of coordination or external driver is needed.
* **Parameterized workers** are not implemented yet. This is a new feature specified in the config schema, which doesn't have any precedent on Cloudflare.
Expand Down
218 changes: 218 additions & 0 deletions src/workerd/server/server-test.c++
Expand Up @@ -1036,6 +1036,63 @@ KJ_TEST("Server: capability bindings") {
)"_blockquote);
}

KJ_TEST("Server: cyclic bindings") {
TestServer test(R"((
services = [
( name = "service1",
worker = (
compatibilityDate = "2022-08-17",
modules = [
( name = "main.js",
esModule =
`export default {
` async fetch(request, env) {
` if (request.url.endsWith("/done")) {
` return new Response("!");
` } else {
` let resp2 = await env.service2.fetch(request);
` let text = await resp2.text();
` return new Response("Hello " + text);
` }
` }
`}
)
],
bindings = [(name = "service2", service = "service2")]
)
),
( name = "service2",
worker = (
compatibilityDate = "2022-08-17",
modules = [
( name = "main.js",
esModule =
`export default {
` async fetch(request, env) {
` let resp2 = await env.service1.fetch("http://foo/done");
` let text = await resp2.text();
` return new Response("World" + text);
` }
`}
)
],
bindings = [(name = "service1", service = "service1")]
)
),
],
sockets = [
( name = "main",
address = "test-addr",
service = "service1"
)
]
))"_kj);

test.start();
auto conn = test.connect("test-addr");
conn.httpGet200("/", "Hello World!");
}

KJ_TEST("Server: named entrypoints") {
TestServer test(R"((
services = [
Expand Down Expand Up @@ -1090,6 +1147,167 @@ KJ_TEST("Server: named entrypoints") {
}
}

KJ_TEST("Server: invalid entrypoint") {
TestServer test(R"((
services = [
( name = "hello",
worker = (
compatibilityDate = "2022-08-17",
modules = [
( name = "main.js",
esModule =
`export default {
` async fetch(request, env) {
` return env.svc.fetch(request);
` }
`}
)
],
bindings = [(name = "svc", service = (name = "hello", entrypoint = "bar"))],
)
),
],
sockets = [
( name = "main", address = "test-addr", service = "hello" ),
( name = "alt1", address = "foo-addr", service = (name = "hello", entrypoint = "foo")),
]
))"_kj);

test.expectErrors(
"Worker \"hello\"'s binding \"svc\" refers to service \"hello\" with a named entrypoint "
"\"bar\", but \"hello\" has no such named entrypoint.\n"
"Socket \"alt1\" refers to service \"hello\" with a named entrypoint \"foo\", but \"hello\" "
"has no such named entrypoint.\n");
}

KJ_TEST("Server: Durable Objects") {
TestServer test(R"((
services = [
( name = "hello",
worker = (
compatibilityDate = "2022-08-17",
modules = [
( name = "main.js",
esModule =
`export default {
` async fetch(request, env) {
` let id = env.ns.idFromName(request.url)
` let actor = env.ns.get(id)
` return await actor.fetch(request)
` }
`}
`export class MyActorClass {
` constructor(state, env) {
` this.storage = state.storage;
` this.id = state.id;
` }
` async fetch(request) {
` let count = (await this.storage.get("foo")) || 0;
` this.storage.put("foo", count + 1);
` return new Response(this.id + ": " + request.url + " " + count);
` }
`}
)
],
bindings = [(name = "ns", durableObjectNamespace = "MyActorClass")],
durableObjectNamespaces = [
( className = "MyActorClass",
uniqueKey = "mykey",
)
],
durableObjectStorage = (inMemory = void)
)
),
],
sockets = [
( name = "main",
address = "test-addr",
service = "hello"
)
]
))"_kj);

test.start();
auto conn = test.connect("test-addr");
conn.httpGet200("/",
"59002eb8cf872e541722977a258a12d6a93bbe8192b502e1c0cb250aa91af234: http://foo/ 0");
conn.httpGet200("/",
"59002eb8cf872e541722977a258a12d6a93bbe8192b502e1c0cb250aa91af234: http://foo/ 1");
conn.httpGet200("/",
"59002eb8cf872e541722977a258a12d6a93bbe8192b502e1c0cb250aa91af234: http://foo/ 2");
conn.httpGet200("/bar",
"02b496f65dd35cbac90e3e72dc5a398ee93926ea4a3821e26677082d2e6f9b79: http://foo/bar 0");
conn.httpGet200("/bar",
"02b496f65dd35cbac90e3e72dc5a398ee93926ea4a3821e26677082d2e6f9b79: http://foo/bar 1");
conn.httpGet200("/",
"59002eb8cf872e541722977a258a12d6a93bbe8192b502e1c0cb250aa91af234: http://foo/ 3");
conn.httpGet200("/bar",
"02b496f65dd35cbac90e3e72dc5a398ee93926ea4a3821e26677082d2e6f9b79: http://foo/bar 2");
}

KJ_TEST("Server: Ephemeral Objects") {
TestServer test(R"((
services = [
( name = "hello",
worker = (
compatibilityDate = "2022-08-17",
modules = [
( name = "main.js",
esModule =
`export default {
` async fetch(request, env) {
` let actor = env.ns.get(request.url)
` return await actor.fetch(request)
` }
`}
`export class MyActorClass {
` constructor(state, env) {
` if (state.storage) throw new Error("storage shouldn't be present");
` this.id = state.id;
` this.count = 0;
` }
` async fetch(request) {
` return new Response(this.id + ": " + request.url + " " + this.count++);
` }
`}
)
],
bindings = [(name = "ns", durableObjectNamespace = "MyActorClass")],
durableObjectNamespaces = [
( className = "MyActorClass",
ephemeralLocal = void,
)
],
durableObjectStorage = (inMemory = void)
)
),
],
sockets = [
( name = "main",
address = "test-addr",
service = "hello"
)
]
))"_kj);

test.start();
auto conn = test.connect("test-addr");
conn.httpGet200("/",
"http://foo/: http://foo/ 0");
conn.httpGet200("/",
"http://foo/: http://foo/ 1");
conn.httpGet200("/",
"http://foo/: http://foo/ 2");
conn.httpGet200("/bar",
"http://foo/bar: http://foo/bar 0");
conn.httpGet200("/bar",
"http://foo/bar: http://foo/bar 1");
conn.httpGet200("/",
"http://foo/: http://foo/ 3");
conn.httpGet200("/bar",
"http://foo/bar: http://foo/bar 2");
}

// =======================================================================================
// Test HttpOptions on receive

Expand Down