-
Notifications
You must be signed in to change notification settings - Fork 373
Experimental feature: Durable Object Facets #4123
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
Open
kentonv
wants to merge
13
commits into
main
Choose a base branch
from
kenton/facets
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
…lass. The goal is to make `ActorNamespace` independent of `WorkerService`, although this doesn't get all the way there yet. The reason we want `ActorNamespace` to be independent is that multifaceted actors may contain instances of classes from multiple services.
…d scheduler. The actor storage should actually originate from the namespace's parent service and NOT from the service that hosts the actor class. The alarm scheduler is a server-wide singleton so it doesn't matter where we get it from. Might as well come with the storage. This eliminates dependencies on the ActorClass's service for any purpose other than constructing the Actor object.
This is a binding that points to a DO class _without_ a storage namespace attached. It's more like a service binding than a DO namespace binding. DO class bindings will be used with facets.
With facets, each facet could have a different class, so we can't get this from the parent namespace anymore.
There's not yet any API to instantiate them. This is just setting up the scaffolding.
From the design doc: ```js // The main interface to manage facets is on `ctx`. let facets = this.ctx.facets; // Each facet has a name, which identifies its respective slice of storage. Names are hierarchical: // if the main facet creates a facet called "foo", and that facet in turn creates a facet called // "bar", the latter facet's true name is "foo/bar". A facet cannot directly access its siblings, // unless the common parent facet chooses to pass references explicitly. // // Names are limited to 128 characters and cannot contain control characters nor `/`. let name = "foo"; // To use a facet, pass the name to `this.ctx.facets.get()`. // // The second parameter specifies information needed to start up the facet. If the facet is already // running, and matches the given parameters, the already-running instance is returned. If the // parameters have changed, then the existing instance will shut down and will reload using the // new parameters. // // Like with regular Durable Objects, there is no explicit "create" operation for a facet. It is // implicitly created when first used, and is implicitly deleted if it shuts down with nothing left // in storage. let facet = this.ctx.facets.get(name, { // The class to use. This is a facet binding, which points to a class that may be defined // in a different Worker. As usual, ctx.exports can be used to refer to other classes // exported by the current worker. Dynamic Dispatch can also look up facets using // `getFacetClass`. class: this.env.FOO_FACET_CLASS, // The value that the facet sees in `ctx.id`. This can be a real Durable Object ID, a plain // string, or null. In this example, the parent chooses to pass its own ID down to the // facet, but this may or may not make sense depending on the use case. If not specified, // defaults to null. id: this.ctx.id }); // `facet` acts like a DO stub, except all calls are local. await facet.someRpcMethod(); // `facets.abort()` forcefully aborts the facet immediately. No further code will execute in the // facet until it is started again. The next call to `facets.get()` is guaranteed to call the // callback and start a new instance. `reason` is thrown by any outstanding or future RPC calls // on existing stubs pointing into the facet. // // This also transitively aborts all children of the facet. this.ctx.facets.abort(name, reason); // Deleting a facet aborts the facet if it is running and then deletes its underlying storage. This // applies transitively to all children as well. this.ctx.facets.delete(name); // Note that `storage.deleteAll()` deletes all facets in addition to regular storage. this.ctx.storage.deleteAll(); ```
This commit was assisted by Claude Code. I wrote the header file, then prompted it to fill in the implementation and test. There was a lot of iteration on the details, culminating in me heavily editing the implementation by hand. I'm not sure if it saved time.
Each actor namespace has always had its own directory on disk, but previously it did not create a kj::Directory for it. Instead, a single SqliteDatabase::Vfs was created for the parent of all the actor storage directories, and each sqlite file was opened by path that included the actor namespace directory. That had two problems: * It's ugly that an ActorNamespace could open files outside its own storage, and just has to promise to always use the correct path to its storage. * We actually didn't give the ActorNamespace a kj::Directory at all, just to the single SqliteDatabase::Vfs. But we now want to open other kinds of files in this directory, so we need a kj::Directory. Let's just actually open the directory specific to the namespace, and create a separate Vfs per-namespace.
The generated output of |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This is a new experimental Durable Objects feature. It requires the
--experimental
CLI flag andexperimental
compat flag to use. I fully expect that we may end up changing the design once we've played with it, and that's fine. This isn't intended to become generally available any time soon.Feature Specification
Multifaceted Durable Objects are Durable Objects (DOs) which are composed of multiple "facets" implemented by different isolates. Each facet is written like a regular DO class, but all the facets run together on the same machine to implement the DO. Each facet has its own storage in the form of a SQLite database, but these databases are all stored together as one logical object.
A multifaceted DO has a "main" facet which implements its public interface. This works exactly like a regular Durable Object. The main facet's implementation can call out to other "facets" as needed, using
ctx.facets
:Notes:
facets.abort()
orfacets.delete()
on it, this does not break the entire actor, only the specific facet.Storage Details
In workerd, a DO's main database has always been stored in a file called
<actor-id>.sqlite
. Each non-root facet is stored in a separate file<actor-id>.<facet-id>.sqlite
, where<facet-id>
is a small integer assigned to each facet. An index of facet IDs is maintained in a separate file,<actor-id>.facets
. This file contains a simple capnp-encoded representation of the facet tree, covering at least all facets for which files exist on disk. This index enables us to avoid encoding facet names into file names, and makes it possible to list and delete facets without performing a full directory listing (which may contain files for unrelated actors).In production (not yet implemented), facets will only be supported when using SRS to store actor data (not the old storage backend). Each facet is stored as a separate SRS "lane". Lane names are prefixed by their facet path, which is the list of facet names leading from the root to the specific facet, separated by
/
characters.