Skip to content
This repository was archived by the owner on Nov 17, 2018. It is now read-only.

Conversation

@rynowak
Copy link
Member

@rynowak rynowak commented Feb 23, 2018

Resolves #62

@rynowak
Copy link
Member Author

rynowak commented Feb 23, 2018

/cc @reisenberger

using System;
using System.Net.Http;

namespace Polly
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My goal is to avoid polluting System.Net.Http. We could also use Microsoft.Extensions.Http but it seems more natural to use the Polly namespace since you'd likely have it already added if you're interested in the context. Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the metapackage approach, it means that a lot of users have will have this package available without realizing it. This is ultimately going to be included in the Microsoft.AspNetCore.All package.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re:

goal is to avoid polluting System.Net.Http. [...] seems more natural to use the Polly namespace

👍 So the extension methods would not appear unless someone had using Polly;, not confusing those not interested.

/// to executing a <see cref="Policy"/>, if one does not already exist. The <see cref="Context"/> will be provided
/// to the policy for use inside the <see cref="Policy"/> and in other message handlers.
/// </remarks>
public static Context GetPollyContext(this HttpRequestMessage request)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on naming? I also considered GetPolicyContext, which I kinda like better on second thought.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also give this a scarier name like GetPolicyExecutionContext to act as salt.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My vote on naming is for GetPolicyExecutionContext(): reinforces the idea that the context is execution-scoped. If so, should the change flow elsewhere also?, eg:

internal static readonly string PolicyExecutionContextKey = "PolicyExecutionContext";

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, will make this change.

var context = request.GetPollyContext();
if (context == null)
{
context = new Context(Guid.NewGuid().ToString());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestions how to create the executioncontextid?

Copy link

@Yves57 Yves57 Feb 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rynowak Just for curiosity, why is there always a Context creation for each request execution (with Context and string allocation), even for uses that don't use it? Why didn't you prefer something like

public static Context GetPollyContext(this HttpRequestMessage request)
{
   if (!request.Properties.TryGetValue(PollyContextKey, out var context))
   {
       context = new Context(Guid.NewGuid().ToString());
       request.Properties[PollyContextKey] = context;
   }
   return context;
}

so the context is allocated only if the user ask for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetPollyContext() is doing the TryGet() here and SetPollyContext() is going the set when one needs to be made. In that example it's not really getting, because it sets if it's not already there. Your example would be more EnsurePollyContext().

Copy link
Contributor

@reisenberger reisenberger Feb 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestions how to create the executioncontextid?

If the user has attached no context to the request, just:

context = new Context();

Context.ExecutionKey is the key set via constructor. This represents the call site usage of the policy. Not-set is allowed; if the user has not chosen to tag Polly logging/telemetry by call site tags (by using request.SetPollyContext(new Context("MyUsageTag"));), we should not set it to something arbitrary.

Compare: Context.ExecutionGuid is the separate, per-execution-unique correlation id. That is automatically initialised by Polly.

EDIT for clarity: A context instance is needed, so that the same Context instance with same Context.ExecutionGuid flows through the whole execution. But we shouldn't set the Context.ExecutionKey if the user doesn't. I think the naming of those two keys on Context doesn't help here, and we could improve it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Yves57 Re:

why is there always a Context creation for each request execution [...] even for uses that don't use it?
Why didn't you prefer [...] so the context is allocated only if the user ask for it?

Polly uses a Context any time executing through a policy, to provide context to telemetry and logging, so the allocation isn't extra; Polly would allocate it later internally if it wasn't done here.

By scoping the context to the HttpRequestMessage, we ensure that the same Context instance is used through the whole chain of DelegatingHandlers when the user has configured multiple PolicyHttpMessageHandler instances. This might be common, for example using both a circuit-breaker and retry policy: .AddPolicyHandler(/* some circuit-breaker */).AddPolicyHandler(/* some retry policy */).

While the motivation was to make that Context consistent through a chain of PolicyHttpMessageHandlers, it will actually mean fewer allocations: if Context wasn't scoped to HttpRequestMessage, the execution through each policy would generate a fresh Context, meaning more allocations.

Good question; hope this explanation helps.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reisenberger Thank you for the precise answer! Looking in the Polly code, I have seen that the "default" context that would be allocated. So I understand now that the best is to allocate it here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reisenberger - FYI it will be new Context(null) the no-arg version is internal.

request.SetPollyContext(context);
}

return _policy.ExecuteAsync((c, ct) => SendCoreAsync(request, c, ct), context, cancellationToken);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I'm explicitly not cleaning up the context here. It's not clear that there's any value to that, but I'm interested in thoughts.

I have a suspicion that we might want to... but it would only affect some truly weird scenarios. Like if someone has implemented their own retry support, but they aren't setting the context themself.

Copy link
Contributor

@reisenberger reisenberger Feb 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explicitly not cleaning up the context here

It seems correct (in fact important) that we don't clean up the context. This line in execution could be hit within the looping flowing from an enclosing retry policy. We'd want the same context to hold through all retries, so we shouldn't (for example) set it to null here. (That iiuc your thought re 'clean up context' - come back on this if I'm missing something, or if you've seen scenarios I haven't that might not play well with that.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok good, sounds like this is right then.

throw new ArgumentNullException(nameof(context));
}

return base.SendAsync(request, cancellationToken);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this seems like a good refactor. Anyone who needs to subclass can override to run functionality 'inside' the policy. Also allowed me to clean up tests a bit.

}

[Fact]
public async Task SendAsync_ExistingContext_ReusesContext()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test as is does not check that the correct context is passed to SendAsync? SUT could push any other context onto HttpRequestMessage before calling SendAsync(...), pop it off again after, and the test would still pass? Could strengthen by tracking Context contextPassedToSendAsync;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, this got missed somehow.

handler.OnSendAsync = (req, c, ct) =>
{
Assert.NotNull(c);
Assert.Same(c, req.GetPollyContext());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contextPassedToSendAsync = c;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is a bit stale now, but I addressed the underlying problem

// Act
var response = await handler.SendAsync(request, CancellationToken.None);

// Assert
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert.AreSame(context, contextPassedToSendAsync);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is a bit stale now, but I addressed the underlying problem

/// Extension methods for <see cref="HttpRequestMessage"/> Polly integration.
/// </summary>
public static class HttpRequestMessageExtensions
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome to see the Context support going in; great that we can add xtn methods to HttpRequestMessage to hide the details.

@rynowak
Copy link
Member Author

rynowak commented Feb 26, 2018

Updated and addressed feedback. If there's nothing else important to take action on here, I plan to merge this tomorrow.

@rynowak rynowak merged commit d266697 into dev Feb 28, 2018
@rynowak rynowak deleted the rynowak/polly-context branch February 28, 2018 03:02
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants