Lifecycle
When you call another Worker over RPC using a Service binding, you are using memory in the Worker you are calling. Consider the following example:
Assume that findUser()
on the server side returns an object extending RpcTarget
, thus user
on the client side ends up being a stub pointing to that remote object.
As long as the stub still exists on the client, the corresponding object on the server cannot be garbage collected. But, each isolate has its own garbage collector which cannot see into other isolates. So, in order for the server's isolate to know that the object can be collected, the calling isolate must send it an explicit signal saying so, called "disposing" the stub.
In many cases (described below), the system will automatically realize when a stub is no longer needed, and will dispose it automatically. However, for best performance, your code should dispose stubs explicitly when it is done with them.
To ensure resources are properly disposed of, you should use Explicit Resource Management ↗, a new JavaScript language feature that allows you to explicitly signal when resources can be disposed of. Explicit Resource Management is a Stage 3 TC39 proposal — it is coming to V8 soon ↗.
Explicit Resource Management adds the following language features:
If a variable is declared with using
, when the variable is no longer in scope, the variable's disposer will be invoked. For example:
using
declarations are useful to make sure you can't forget to dispose stubs — even if your code is interrupted by an exception.
Because it has not yet landed in V8, the using
keyword is not yet available directly in the Workers runtime. To use it in your code, you must use a prerelease version of the Wrangler CLI to run and deploy your Worker:
This version of Wrangler will transpile using
into direct calls to Symbol.dispose()
, before running your code or deploying it to Cloudflare.
The following code:
...is equivalent to:
The RPC system automatically disposes of stubs in the following cases:
When an event handler is "done", any stubs created as part of the event are automatically disposed.
For example, consider a fetch()
handler which handles incoming HTTP events. The handler may make outgoing RPCs as part of handling the event, and those may return stubs. When the final HTTP response is sent, the handler is "done", and all stubs are immediately disposed.
More precisely, the event has an "execution context", which begins when the handler is first invoked, and ends when the HTTP response is sent. The execution context may also end early if the client disconnects before receiving a response, or it can be extended past its normal end point by calling ctx.waitUntil()
.
For example, the Worker below does not make use of the using
declaration, but stubs will be disposed of once the fetch()
handler returns a response:
A Worker invoked via RPC also has an execution context. The context begins when an RPC method on a WorkerEntrypoint
is invoked. If no stubs are passed in the parameters or results of this RPC, the context ends (the event is "done") when the RPC returns. However, if any stubs are passed, then the execution context is implicitly extended until all such stubs are disposed (and all calls made through them have returned). As with HTTP, if the client disconnects, the server's execution context is canceled immediately, regardless of whether stubs still exist. A client that is itself another Worker is considered to have disconnected when its own execution context ends. Again, the context can be extended with ctx.waitUntil()
.
When stubs are received in the parameters of an RPC, those stubs are automatically disposed when the call returns. If you wish to keep the stubs longer than that, you must call the dup()
method on them.
When an RPC returns any kind of object, that object will have a disposer added by the system. Disposing it will dispose all stubs returned by the call. For instance, if an RPC returns an array of four stubs, the array itself will have a disposer that disposes all four stubs. The only time the value returned by an RPC does not have a disposer is when it is a primitive value, such as a number or string. These types cannot have disposers added to them, but because these types cannot themselves contain stubs, there is no need for a disposer in this case.
This means you should almost always store the result of an RPC into a using
declaration:
This way, if the result contains any stubs, they will be disposed of. Even if you don't expect the RPC to return stubs, if it returns any kind of an object, it is a good idea to store it into a using
declaration. This way, if the RPC is extended in the future to return stubs, your code is ready.
If you decide you want to keep a returned stub beyond the scope of the using
declaration, you can call dup()
on the stub before the end of the scope. (Remember to explicitly dispose the duplicate later.)
A class that extends RpcTarget
can optionally implement a disposer:
The RpcTarget's disposer runs after the last stub is disposed. Note that the client-side call to the stub's disposer does not wait for the server-side disposer to be called; the server's disposer is called later on. Because of this, any exceptions thrown by the disposer do not propagate to the client; instead, they are reported as uncaught exceptions. Note that an RpcTarget
's disposer must be declared as Symbol.dispose
. Symbol.asyncDispose
is not supported.
Sometimes, you need to pass a stub to a function which will dispose the stub when it is done, but you also want to keep the stub for later use. To solve this problem, you can "dup" the stub:
You can think of dup()
like the Unix system call of the same name ↗: it creates a new handle pointing at the same target, which must be independently closed (disposed).
If the instance of the RpcTarget
class that the stubs point to has a disposer, the disposer will only be invoked when all duplicates have been disposed. However, this only applies to duplicates that originate from the same stub. If the same instance of RpcTarget
is passed over RPC multiple times, a new stub is created each time, and these are not considered duplicates of each other. Thus, the disposer will be invoked once for each time the RpcTarget
was sent.
In order to avoid this situation, you can manually create a stub locally, and then pass the stub across RPC multiple times. When passing a stub over RPC, ownership of the stub transfers to the recipient, so you must make a dup()
for each time you send it: