-
Notifications
You must be signed in to change notification settings - Fork 296
Immutable Context chain #508
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
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 sure I 100% understand the utility of an immutable Context
, but if that's the way we want to go, this implementation looks pretty good. I had some nits.
Co-authored-by: Ryan Levick <[email protected]>
Co-authored-by: Ryan Levick <[email protected]>
I've changed the logic to get rid of that pesky |
|
||
options.decorate_request(&mut request)?; | ||
let response = self | ||
.pipeline() | ||
.send(&mut pipeline_context, &mut request) | ||
.send(&ctx.create_override(), &mut request) |
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 reads like creating an override without actually overriding anything.
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.
Good point. It's an unpleasant side effect of being unable to transform Context
into a trait object 😢. If it were possible, send
would accept it instead of a concrete OverridableContext
instance.
I'll try to come up with something better!
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.
@thovoll: what do you think now? I've replaced create_override()
with into()
when you do not want to override anything.
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.
Thanks @MindFlavor, looks better to me.
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 feel like this may not be the right direction for us to go at this moment. Unlike Golang or C# the Context
type is scoped only to our SDK, and is not a shared construct among libraries. The main case we need to cover for is being able to construct a Context
instance once, and copy standalone instances of it that we can hand out to individual requests. We already support this through our #[derive(Clone)] Context {}
implementation.
Within the pipeline construct of the SDK, the point of having a Context
is that to enable shared mutable state on a per-request basis. Clone-on-Write semantics don't make much sense within a request pipeline, and if I'm reading the current implementation right, might even lead to performance degradation. I agree we should revisit the relationship between Pipeline
and Context
, but I believe there may be alternative ways of achieving similar results.
I feel like I've mentioned this before, but I would prefer it if we gained understanding the full range of uses of it before we propose any further changes to it. As I mentioned in #504, the final question we appear to need to answer about Context
usage is whether it needs to interact with (distributed) tracing. I've added that to the agenda for tomorrow's meeting.
So to summarize: I think we should hold off on deciding whether we want to take Context
in this direction until we have more information on how Context
and distributed tracing interact.
I do not think it's the whole story. Quoting @JeffreyRichter's comment (in #504 (comment)):
That is exactly what this PR provides: a stack based type map. Each policy in the pipeline can add a value and it will get "propagated" to the following policies but it will get "removed" as soon as the stack unwinds. A simple shared mutable state won't work because any change would be irreversible.
The
I agree. I am translating into code the ideas we are discussing because I think it's easier to reason on pros and cons of an approach while reading real, working code. Also, I want this discussion to be in the open: this is an important design decision that could benefit from every input and should remain "documented" for posterity. |
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 agree with @yoshuawuyts that we probably want to make sure we fully understand the use case for Context
before we decide on which direction to go with, but the implementation here got much more complicated than it was last time I reviewed, and I'm not convinced it needed to. There's now two Context
types again and a trait to unify them, and I'm unsure why.
/// do that, `C2.get::<Struct2>()` will give you `Solar system`, while `C1.get::<Struct2>()` will | ||
/// give you `World`. | ||
#[derive(Debug)] | ||
pub struct OverridableContext<'a> { |
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 don't really get why we need two types Context
and OverrideableContext
. Is this just to ensure that one is immutable and the other isn't? Wouldn't the fact that the send
method takes an immutable reference guarantee that?
/// we can replace `Struct2` with `Solar system` without losing the original `C1`'s `World`. If we | ||
/// do that, `C2.get::<Struct2>()` will give you `Solar system`, while `C1.get::<Struct2>()` will | ||
/// give you `World`. | ||
pub trait TypeMapContext { |
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.
Why is this trait needed? Is this just to ensure that Context
and OverrideableContext
have the same methods? We don't seem to ever require that in code and so this feels like it just complicates things.
@@ -74,7 +63,7 @@ impl Policy<CosmosContext> for AuthorizationPolicy { | |||
generate_authorization( | |||
&self.authorization_token, | |||
&request.method(), | |||
&ctx.get_contents().resource_type, | |||
ctx.get().expect("Cosmos pipeline bug: ResourceType must be present in the context at this point"), |
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.
Using Context
for something that's required feels wrong. It would be nice if this remained something that is checked at compile time.
I'm going to close this and reopen a new PR following a discussion with @JeffreyRichter. Key points:
|
Intro
Following @JeffreyRichter's comment on PR #504 this PR proposes:
Context
. Each policy can add information to theContext
without modifying the originalContext
.Policy
'ssend
function no longer accepts a mutableContext
instance (because they are immutable).PipelineContext
(obsoleted by the newContext
type map).Policy
'ssend
function no longer accepts aPipelineContext<T>
but a much simplerContext
.Immutable
Context
The
Context
must be immutable in the sense that policies can add information "overriding" the preexisting one. This is done by implementing aContext
stack with topmost winner policy. In other words, the final type map is the union of every type map in the stack, picking only the topmost entity in case of collisions.Policies can change the contents of a received
Context
by wrapping it with the provided function. The newContext
can then by defined asmut
able. Every operation on the newContext
does not change the underlying one(s).In order to keep the original
Context
clonable but to prevent the cloning of the overiddenContext
, I've introduced another structureOverridableContext
that performs the "type overriding" function without cloning. For efficiency the wrapping structure only gets a reference of the wrapped one (with guaranteed lifetime match since we are strictly walking the stack).Since we are talking about "stack based" override, to get the previous value you just need to drop the overriding context (or let it go out of scope).
See this example taken by the added test case. Here we are simulating two pipeline policies. The first
Context
is provided by the SDK user (viaContext::new()
). The following policies can override the value by simply callingcontext.create_override()
. The struct will take care of everything else:Removal of
PipelineContext
(obsoleted by the newContext
type map).This changes removes a lot of useless - and confusing - code. Now the
Context
type map is able to store arbitrary data so thePipelineContext
is no longer needed.See the Data Lake code, is a lot leaner as a result.