-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Feature: Per-Grain-class Request Interceptors #965
Feature: Per-Grain-class Request Interceptors #965
Conversation
var interfaceIdArgument = parameters[1].Name.ToIdentifierName(); | ||
var methodIdArgument = parameters[2].Name.ToIdentifierName(); | ||
var argumentsArgument = parameters[3].Name.ToIdentifierName(); | ||
var requestArgument = parameters[1].Name.ToIdentifierName(); |
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.
We create local variables to hold request.InterfaceId
, etc.
Concerns:
Alternative: public interface IInterceptor : IGrain where T : IGrain If instead of a Invoke call to override in the grain, the grain can have a protected virtual property containing the interceptor. public abstract class Grain : IAddressable After constructing a grain of type G, if the interceptor property is empty (developer did not override it), the grain construction can use the ServiceProvider to create an implementation of IIntercepter. If one is found, the interceptors Intercepted property is set to the grain and the grain's Interceptor property is set to the interceptor. When invoking grain calls, if the interceptor is set, it is passed to the invoker's invoke call, rather than the grain, if it is not, the grain is used, as usual. The interceptor implementation would simply be a decorator that conforms to the grain's interface and the Interceptor interface. It would intercept each grain call, do what it wants, then passed the call through to the real grain (set in the Intercepted property). This allows grain developers to provide their own interceptors, or for interceptors to be injected after the fact via the service provider (for monitoring, debugging, fault injection, testing, ...) Thoughts? jbragg |
@jason-bragg thanks for the input :) Grain developers don't need to provide the The public class InterceptRequestGrain : Grain, ISomeGrain
{
protected override Task<object> Invoke(InvokeMethodRequest request, IGrainMethodInvoker invoker)
{
if (this.Invoker != null)
{
return invoker.Invoke(Interceptor, request);
}
return base.Invoke(request, invoker);
}
protected virtual ISomeGrain Interceptor { get; set; }
// Other methods...
}
I am open to your suggestion @jason-bragg. @yevhen: do you have anything to add? |
quite opposite. It covers all possible use-cases I can come up with.
As @ReubenBond already said that's not the case.
That's exactly the situation were trying to avoid with proposed design. The proposed design is actually a better more lean implementation of decorator pattern. Consider the interface below: public interface ISomeGrain : IGrain
{
Task Foo();
Task Bar(string x);
} To implement simple logging, with the design that you' suggesting I will need to hand-write an implementation like the one below: public class SomeGrainDecorator : Grain, ISomeGrain
{
ISomeGrain next;
public SomeGrainDecorator(ISomeGrain next)
{
this.next = next;
}
public Task Foo()
{
// log before
return next.Foo();
// log after
}
public Task Bar(string x)
{
// log before
return next.Foo(x);
// log after
}
} I will basically need to hand-write and duplicate every method for every grain interface in the system, if I want to log all calls to a grains. Imagine a system with tens of interfaces and hundreds of methods. That will be an incredible (wasted) effort. On contrary, with the @ReubenBond proposed design, I will be able to simply create a base class from which all my grains will inherit from and put this logic in a single place. public abstract class AppGrainBase : Grain
{
override Task<object> Invoke(InvokeMethodRequest request, IGrainMethodInvoker invoker)
{
// log before
return invoker.Invoke(Interceptor, request);;
// log after
}
} @ReubenBond I don't have anything else to add |
@Plasma will that be enough to fulfill you request about server-side interception? |
@@ -265,7 +290,7 @@ private static MethodDeclarationSyntax GenerateInvokeMethod(Type grainType, Meth | |||
Type grainType, | |||
IdentifierNameSyntax grain, | |||
MethodInfo method, | |||
IdentifierNameSyntax arguments) | |||
ExpressionSyntax arguments) |
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.
Could you explain about what this part of the change does please?
s/IdentifierNameSyntax arguments/ExpressionSyntax arguments/
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.
The change was introduced when I was (temporarily) using an expression (method.Arguments
) instead of a variable (arguments
). It is no longer needed, but I should have been using that type there anyhow - IdentifierNameSyntax
is a sub-class of ExpressionSyntax
and the code requires an expression, not an identifier.
Documenting some design sanity check items that i looked at, mostly for the record:
Other comments: The question about which specific "interceptor API" should be used is hard to make without a clear statement of the real world usage scenarios. Issues #749 and #963 don't really give enough scenario background to fully clarify the design intent for this change IMHO. Thoughts? |
@ReubenBond & @yevhen All good points. "I will basically need to hand-write and duplicate every method for every grain interface in the system" I'll take a step back from implementation details and explore the usage pattern for interceptors that I am working from. I would expect someone to be able to write a grain that implements a grain interface without concern for intercepting calls. Then, once there is a need to intercept a grain's calls, write an interceptor for the grain that the system somehow magically uses. For interceptors I was expecting for a way in which someone could add interceptors after the fact without changing the grain. It seems to me that intercepting grain call is about injecting some logic in-between the caller and called behavior. To require the interceptor to be part of the intercepted grain, to me, negates much of it's value. I would expect someone to be able to write something like the below. public interface ISomeGrain : IGrain Interceptor could hook all calls public class SomeGrainLogInterceptor : IIntercepeter or hook each call public class SomeGrainCallMonitorInterceptor : Intercepeter, ISomeGrain Is the above a reasonable expectation for interceptors? |
@jthelin "But it feels like we need to surface more of the usage scenarios" +1 |
I very much appreciate your input, @jason-bragg, @jthelin, & @yevhen :) Here's a few usage scenarios from the top of my head:
It enables a degree of Aspect Oriented Programming. Things like Event Sourcing may become easier to implement, too. We can ensure that all outstanding events are applied before invoking a method. We can also reset an activation if an exception is thrown (depends on desired semantics/coding practices). Of course, these things may not be desirable for everyone, but this PR puts the choice in users' hands. Some of those things become easier with additional information. For example, I would like to be able to get the corresponding EDIT: Many of the Cross-cutting Concerns listed in this Wikipedia article can be solved using grain interceptors |
@yevhen Thanks for the ping - I think for me #963 solves my use case more elegantly, but if this Invoke() method was added to IGrain then I would just add this logic to each grain base class as needed, so either way I am not as fussed (#963 seems a bit cleaner given how client interceptors work?). The logic I would be doing would roughly be extracting the User ID from RequestContext and setting up a Principal object. I agree with the above points that the decorator pattern for intercepting ("I will basically need to hand-write and duplicate every method for every grain interface in the system") is not the way to go. Great PRs thanks Reuben. |
My main issue with this PR is the invoker that is passed to the interceptor code. First, code wise, I don't think More importantly, interceptor for me is a code that executes before the call but does no change where the call goes, can not substitute the call itself, and is not to be used for "routing" requests differently then the framework intended (even the behavior on thrown exception should be frameworks decision). The way interceptor is written now, developers will be able to do so. Maybe we don't intend them to, but they will be able to not invoke the actual call, or even implement their whole app logic inside the single interceptor method, thus basically throwing away the strong typeness of the arguments, thus going (backwards, in my opinion) to Akka/Erlang weak typed actors. For interceptor I would stick with the same method signature as we have for a global interceptor, returning void, not accepting invoker. Just add a virtual protected function on the grain base. Also, no need to change any code gen. Just call this base function, just like you do for a global interceptor directly. Can even optimize with a precomputed flag if the override exist or not. Additionally, it is not even clear, after @Plasma's response, that anyone actually needs the per type interceptor. The global interceptor (#963) looks like enough. |
I'm happy either way. We could hide the invoker or we could split this into I much prefer |
Meh |
Ultimately we cannot stop users from hurting themselves while still letting them execute code. The goal is only to discourage bad behavior. How about something similar to this in protected virtual Task<object> Invoke(InvokeMethodRequest request)
{
return this.currentInvoker.Invoke(this, request);
}
internal Task<object> Invoke(InvokeMethodRequest request, IGrainMethodInvoker invoker)
{
// BTW: this could go on IActivationData, but there is a comment telling me that we should not
this.currentInvoker = invoker;
return this.Invoke(request);
} That way, users must call BTW, it may make more sense to access the method invoker from the // Do not use this directly because we currently don't provide a way to inject it;
// any interaction with it will result in non unit-testable code. Any behaviour that can be accessed
// from within client code (including subclasses of this class), should be exposed through IGrainRuntime.
// The better solution is to refactor this interface and make it injectable through the constructor.
internal IActivationData Data; Not 100% sure what "directly" means in that context... the line below it is this: internal GrainReference GrainReference { get { return Data.GrainReference; } } We can solve this without bike-shedding. |
I'll add a few existing interceptor implementations to facililate discussion: Both of them has capabilities listed and some rationale on them. Altering the flow of call is something people expect, I believe. For instance the case with @Plasma where a request could be aborted with an authorization failure. I could think of a few more use cases such as caching and error handling. Has anyone ideas how to improve and utilize the first-pass, still rudimentary DI we have? Maybe it'd be beneficial to think this in context of #934 too, even if it broandens the scope. One of the initial ideas, which I'm fond of too, in bringing DI was to make it look like what other parts of .NET stack is likely to use but still taking account the Orleans model. That way we'd benefit from the broader system and make already learned knowledge applicable here too. |
This can already use DI - you have access to your grain, which is constructed using DI. |
base.Invoke(request) is exactly what I have in Orleankka 👍 |
Reuben, can you please describe your scenarios where:
|
For 1) the global interceptor runs before, not around, the invocation, so it cannot do things like ensure that invariants are met. It is also useful for @Plasma's case where he wants to inject a lifetime-scoped using (container.BeginScope())
{
// Similar to Web API's ApiController.User...
this.User = container.ResolveInstance<IPrincipal>();
return await base.Invoke(request);
} It's not so much that we need the return value, but rather that is the most obvious way to implement this and I don't see a compelling reason to hide the return value. The return value can be useful for logging/diagnostics, anyhow. This is structurally similar to the model used in OWIN & ASP.NET 5, by the way |
What about making a copy of local state before every request, say for safety/purity reasons? It will be a 10 min exercise for anyone who want to implement this, if we have per-grain type interceptor. I can have With event-sourcing and some additional metadata, like knowing when something is a query or command, I can optimize this logic further, and make snapshots of the state only when request is a command (I can inspect it easily).
indeed. It's the same Pipeline model. |
On that note, I would prefer that our global interceptor also used a similar approach: rather than running before a request, it should run around a request like an OWIN/ASP.NET middleware. This allows for greater composability. |
Can you please share a link to OWIN interceptors? |
Ok, so Postsharp has both options:
Both are clearly valuable. "Decorators are faster than interceptors, but interceptors are more powerful". Specifically, in his example he shows things that can be done with interceptor and not with decorator (custom retry policy). Clearly (if we embrace this terminology) I was thinking about pre and post decorators and not about interceptors. Mostly since that is what we had already implemented for client pre-invoke "interceptor", but also since that is what I used in the past in other systems (CORBA) and there is was called pre and post "interceptors". So I guess Reuben is asking for interceptors and not decorators. The argument in favor of interceptors is that it is more powerful. The line goes via an open and willing to listen discussion, where sides can be convinced in the opinion which is different from their initial. We, the Orleans core team, are not vetoing anything. And after reading the examples in Post Sharp, I am more inclined to be convinced that interceptors are more valuable than decorators and that maybe we indeed should embrace them and not decorators. But there are still tradeoffs in my opinion, the choice is not a clear cut, and thus this discussion. |
And thanks @veikkoeeva for sharing the links: And in Logging and Intercepting Database Operations there is yet a third option: pre and post decorators with optional returned result. If the pre decorator returns a result, the actual operation is not invoked and the decorator's result is returned instead. But still, the pre decorator does not invoke the original operation itself (so its not an interceptor in PostSharp terminology). |
I saw some comparisons with Postsharp and OWIN/Nancy on what this PR is all about... So my question is... Is clear the intentions/design on allow only interception of a grain call per grain-type in order to REALL intercept and maybe modify the behavior of the request(maybe return a value and never call the real operation itself)? Or there are some intention on give the user the ability to just execute custom code before and/or after a grain call (such as logging) ? |
@@ -11,6 +11,8 @@ | |||
|
|||
namespace Orleans | |||
{ | |||
using Orleans.CodeGeneration; |
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 is obviously not needed any more.
I suggest IInterceptor -> IGrainInvokeInterceptor. LGTM. I like your solution with an explicit interface. |
Sounds good, @sergeybykov - I'll make the suggested changes soon. Thanks for the CR |
b9b54b8
to
c49a66f
Compare
LGTM on the approach. Would not IInterceptable be a better interface name for the grain to implement? From grain standpoint, it is interceptable. |
I think
What do you think? |
@ReubenBond I had a similar reaction - IInterceptable seems to mean that somebody else can intercept calls to the grain. IWhaeverIntercetor OTOH conveys that it's the grain that intercepts. |
OK, the "standard" in .NET appears to be IInterceptor http://docs.autofac.org/en/latest/advanced/interceptors.html so maybe IGrainInterceptor? Why do we need invoke in it? |
@gabikliot I'm happy with that. Do you think we should rename the method to |
I wasn't implying that. Yes, probably |
can I tap in with the name - |
@centur I see nothing wrong with |
As I said to Reuben - it's your call on naming, I just found all these |
Feature: Per-Grain-class Request Interceptors
Great work @ReubenBond, thanks! |
👍 Now we can start bringing Orleankka pieces in. :-) |
Thank you, @ReubenBond. Officially top commented PR to the Orleans of all time. |
Amazing; thank you @ReubenBond and team. 👍 |
Good milestone indeed @ReubenBond ! |
Congrats! 🎉 |
I'll make the sensation of achievement one more 👍 longer. :) |
Now that this was merged we also need to update documentation: |
Hello and Good Morning Everyone. |
@edikep2000 You need to add https://www.nuget.org/packages/Microsoft.Orleans.OrleansCodeGenerator/ to your silo and client processes. Though I think you better open a new issue, as this one seems unrelated. |
This feature allows users to override
Grain.Invoke(InvokeMethodRequest request, IGrainMethodInvoker invoker)
in order to perform request interception on a per-Grain-class level.As requested by @yevhen in #749.
Example: