-
Notifications
You must be signed in to change notification settings - Fork 1
Add Middleware Support for OperationHandlers #33
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
Conversation
…cellation in operation handlers
…tion handlers and the potential race condition if but wait_until_.. and is_cancelled are used at the same time
…. Add test to confirm interceptors work for sync operation handlers
…d up not being very useful. Update sync test to force use of the executor.
c8b3023 to
60ef746
Compare
cretz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM except for some naming things. Ideally we wouldn't add a layer of indirection and stack trace entry for every call just to support middleware, but not a big deal. Would also wait to merge this until the Temporal SDK PR is at least present to confirm the abstraction works as expected.
src/nexusrpc/handler/_core.py
Outdated
| Get the specified handler for the specified operation from the given service_handler and apply all interceptors. | ||
| """ | ||
| op_handler: AwaitableOperationHandler[Any, Any] = ( | ||
| _EnsuredAwaitableOperationHandler( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So there is a bit of a maybe-rare use case here that may struggle - accessing the underlying handler that the user themselves created. Some other SDKs' middleware is given the actual handler. I think we could consider having AwaitableOperationHandler have a def underlying(self) -> Optional[OperationHandler[Any, Any]], and we should make _EnsuredAwaitableOperationHandler return the underlying operation handler. Whether we 1) make the method optional to implement (I think we should consider making it required), and 2) return optional or non-optional (can be self in the rare case they wanted to hide the underlying) can both be subject to discussion.
But ok if we decide not to do anything here too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not against adding a def underlying(self) method, but I think it's a bit confusing for an implementor and users. Is it intended to always return the root level OperationHandler, or just one level below it in the middleware stack? Would the caller of underlying() be expected to call underlying on each layer until it finally gets to the instance it expects?
Do you have a use case in mind that this would be useful for? My current feeling is that if the interceptor relies on the details of a specific operation handler then there is hopefully a better way to do it than that.
It would be nice to provide the actual handler in some ways, but it seems worth it to reduce the surface area of the sync/async handling. If we gave the operation handler directly we would need a way to provide the executor so that middleware implementations could submit the sync work to that and passing it as another arg to intercept felt awkward to me. I also didn't love the idea of every implementation needing to do the async callable check that happens in _EnsuredAwaitableOperationHandler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it intended to always return the root level OperationHandler, or just one level below it in the middleware stack?
Just one below and if all do that, then it is the root one
Do you have a use case in mind that this would be useful for? My current feeling is that if the interceptor relies on the details of a specific operation handler then there is hopefully a better way to do it than that.
Not specifically at this time, but we learned with workflows that people want access to the instance of those in interceptors, so we added Workflow.Instance property.
I agree overall, we can punt on this and if there is value in having the non-middleware-defined OperationHandler instance, maybe we can expose it another way at that time (e.g. via contextvars or something).
…ionHandlerMiddleware.intercept an abstract method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This LGTM. Can you do me a favor before merging and confirm that latest main Temporal Python SDK completely passes tests with the Nexus dependency changed to point to this branch but no other changes? Just want to confirm there are no surprises.
…inition, must always be OperationHandler[Any,Any]
Description
This take on OperationHandler middleware leverages the fact that
Handleruses an executor to invoke operation handler methods to simplify interceptor implementation. Using the executor to run sync handlers effectively narrows the return type at that point toAwaitable[StartOperationSync[OutputT] | StartOperationResultAsync]. Using that we can define anAwaitableOperationHandlerand remove the complexity of sync vs async from interceptors.Change Summary
OperationHandler.startto include the return typeAwaitable[StartOperationResultSync[OutputT] | StartOperationResultAsync]to enable composing OperationHandlers.AwaitableOperationHandlerto use when calling operation handler methods and in interceptors.OperationHandlerInterceptorHandlerto apply any interceptors when looking up the operation handler by service & operation name.closes #32