Skip to content

feat: close a resource at the end of async with - #334

Closed
moetemp wants to merge 1 commit into
moe/up/poll-itemsfrom
moe/up/resource-close
Closed

moetemp wants to merge 1 commit into
moe/up/poll-itemsfrom
moe/up/resource-close

Conversation

@moetemp

@moetemp moetemp commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

What changed?

This PR adds @nexus.close, which marks the method that ends a resource, so async with and await using close it.

/// @nexus.operation "close-stream"
/// @nexus.close failure="failure"
close: func(%result: option<payload>, failure: option<failure>);
  • Python. The resource class gets __aenter__ and __aexit__. At the block's end it asks the support package with _support.exit_close(self, exc) whether to close now, and with which failure, and then calls the close with failure=. One class serves both targets, so the support package owns the policy. If the close fails, its error has the block's error as its __context__, as Python does for a context manager.
  • An operation that returns such a resource returns _support.ResourceCall, so both await client.create_stream(...) and async with client.create_stream(...) as stream work. This is the facade's Creating. The function is a plain function now, and it builds the request when it is called.
  • TypeScript gives the class [Symbol.asyncDispose], and .NET gives it IAsyncDisposable. Both call the close with no failure, since await using doesn't see the error. The TypeScript samples take the esnext.disposable lib for it.
  • Go has no such protocol, since Go code uses defer, and Java has no resources.

nexgen refuses a second @nexus.close on a resource, a required parameter of the method, and a failure that names no parameter of the temporal.api.failure.v1.Failure type.

The sample support gives exit_close and ResourceCall. Outside a Workflow, its policy closes on a clean exit and on an Exception, closes a cancel as Temporal's CancelledError so the Store records canceled, and leaves the resource open for another BaseException, such as KeyboardInterrupt. In Workflow code it registers the handle with the run and doesn't close it, since the SDK may close it at the run's end or keep it open for a retry run.

It sits on #333. The stack is #308, #310, #311, #297, #296, #303, #304, #309, #318, #319, #320, #321, #322, #323, #324, #325, #326, #327, #328, #329, #330, #331, #332, #333, then this PR.

Why?

The Stream API of the proposal creates a Stream in async with, and the block's end closes it with the block's error. The facade has its own Creating and context manager for this today.

How did you test it?

Python tests close a Stream at the end of a block that raises ValueError, of a clean block, and of a canceled block, and check the failure of each close request. Others check that a KeyboardInterrupt leaves the Stream open, that a failed close raises with the block's error as its context, and that Workflow code defers the close. A TypeScript test disposes a Stream and checks the close request, and a .NET compile check closes a Stream with await using. A unit test checks the directive, and rejection fixtures cover each refusal.
cargo validate passes.

`@nexus.close` marks the method that ends a resource. Python closes it at a block's end with the
block's error, TypeScript and .NET close it at the end of `await using`.
@moetemp

moetemp commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Moved to moetemp/nexgen#94 for review first. It comes back here after that.

@moetemp moetemp closed this Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant