Comments (15)
@peter-csala sorry for my own delay in responding, many tasks I am across and put this issue back a bit but I will check this out shortly and get back to you if I can find further information on this. Will be in touch shortly.
from polly.
I will try get a sample application shortly. Thank you!
from polly.
@peter-csala this might take some time to get you a sample application, but Ill endeavor to get this ASAP. Keen to get this sorted.
from polly.
Hi Leonard, I've provided three slightly different implementation examples here how to solve refresh token problem with Polly.
Similar question as your has been asked at end of last year: how to make sure that only a single refresh request is sent out. Here I have provided a sample for that (basically a SemaphoreSlim
and some simple heuristics).
IMHO my suggested solution is a bit more convenient than passing around the HttpClient
as a part of the Context
.
from polly.
@peter-csala thank you for the solution provided!
I'm guessing in my user case I will still need the HttpClient
as part of the Context
for the sake of updating the headers with a new token.
i.e.:
HttpClient httpClient = (HttpClient)context["httpClient"];
if (await localStorageService.GetSecurityTokenAsync().ConfigureAwait(false) is string securityToken
&& !string.IsNullOrEmpty(securityToken))
{
if (httpClient.DefaultRequestHeaders.Contains(Constants.AuthenticationTokenHeaderKey))
{
_ = httpClient.DefaultRequestHeaders.Remove(Constants.AuthenticationTokenHeaderKey);
}
httpClient.DefaultRequestHeaders.Add(Constants.AuthenticationTokenHeaderKey, securityToken);
}
On next retry the HttpClient
will contain the new header with token?
from polly.
On next retry the
HttpClient
will contain the new header with token?
The presented code itself yes it updates the related header in a way that the next attempt could read the refreshed token.
But because it is just a code fragment that's why I can not say that it solves your original question/problem. Your header update logic is not atomic so, multiple threads can update the header simultaneously if it is not treated as a critical section (by protecting with a lock).
As with my suggested sample make sure only one thread can perform the token refresh at the same time.
from polly.
@peter-csala thank you for your quick response 👍
The related code is the following:
public static AsyncRetryPolicy<HttpResponseMessage> AuthEnsuringPolicy = Policy<HttpResponseMessage>
.HandleResult(r => r.StatusCode == HttpStatusCode.Unauthorized)
.RetryAsync(1, onRetryAsync: async (ex, i, context) => _ = await App.ContainerProvider.Resolve<IAuthTokenService>().RefreshTokenAsync((HttpClient)context["httpClient"]).ConfigureAwait(true));
The RefreshTokenAsync
method is this, but basically RefreshAuthorizationTokenAsync
will update local storage on the SecurityToken
so that GetSecurityTokenAsync
will have latest;
public class AuthTokenService : IAuthTokenService
{
private static readonly SemaphoreSlim _semaphoreSlim = new(1);
private DateTime? _lastRefreshed;
public async Task<bool> RefreshTokenAsync(HttpClient httpClient)
{
await _semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
//Use any arbitrary logic to detect simultaneous calls
if (_lastRefreshed.HasValue && _lastRefreshed - DateTime.UtcNow < TimeSpan.FromSeconds(3))
{
Console.WriteLine("No refreshment happened");
return false;
}
ILocalStorageService localStorageService = App.ContainerProvider.Resolve<ILocalStorageService>();
if (localStorageService.GetCustomer() is Customer customer
&& await customer.GetPasswordAsync().ConfigureAwait(true) is string password
&& await App.ContainerProvider.Resolve<IUserManagementService>().RefreshAuthorizationTokenAsync(customer.Mobile, password).ConfigureAwait(true) is not null
&& await localStorageService.GetSecurityTokenAsync().ConfigureAwait(false) is string securityToken
&& !string.IsNullOrEmpty(securityToken))
{
if (httpClient.DefaultRequestHeaders.Contains(Constants.AuthenticationTokenHeaderKey))
{
_ = httpClient.DefaultRequestHeaders.Remove(Constants.AuthenticationTokenHeaderKey);
}
httpClient.DefaultRequestHeaders.Add(Constants.AuthenticationTokenHeaderKey, securityToken);
Debug.WriteLine($"Refreshment happened {DateTime.UtcNow}");
_lastRefreshed = DateTime.UtcNow;
}
}
finally
{
_ = _semaphoreSlim.Release();
}
return false;
}
}
The httpClient.Send
section:
requestMessage.SetPolicyExecutionContext(new Context
{
{ "retrycount", 0 },
{ "httpClient", httpClient }
});
HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(requestMessage).ConfigureAwait(false);
using Stream stream = await httpResponseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false);
using StreamReader steamReader = new(stream);
using JsonTextReader jsonTextReader = new(steamReader);
if (httpResponseMessage.IsSuccessStatusCode)
{
return new MemoryStream(buffer: stream.ToByteArray());
}
Just a side question as well, on the RefreshTokenAsync
is there any reason why the SemaphoreSlim
cant be changed to private static readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
to give it max count? Then the following could be removed that only permit one execution at a time
if (_lastRefreshed.HasValue && _lastRefreshed - DateTime.UtcNow < TimeSpan.FromSeconds(3))
{
Console.WriteLine("No refreshment happened");
return false;
}
from polly.
@LeoJHarris According to my understanding there both of these means "exclusive lock"
private static readonly SemaphoreSlim _semaphoreSlim = new(1);
and
private static readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
When you acquire a token the semaphore is decrementing its counter. So, if we would call the Release
more times than the WaitAsync
then the counter could go higher than 1.
Here is a dotnet fiddle to play with it: https://dotnetfiddle.net/o6vEk2
from polly.
@peter-csala Hi have found the lock to be working correctly, no issues there, however the following seems to be happening when attempting to refresh the token and make subsequent calls again on retry:
First time:
[0:] Did Login 3/13/2024 10:44:28 PM
[0:] Refreshment happened 3/13/2024 10:44:28 PM
[0:] No refreshment happened 3/13/2024 10:44:28 PM
[0:] No refreshment happened 3/13/2024 10:44:28 PM
[0:] No refreshment happened 3/13/2024 10:44:28 PM
[0:] No refreshment happened 3/13/2024 10:44:28 PM
[0:] No refreshment happened 3/13/2024 10:44:28 PM
[0:] No refreshment happened 3/13/2024 10:44:28 PM
Later on this is called again shortly after on retry:
[0:] Did Login 3/13/2024 10:44:40 PM
[0:] Refreshment happened 3/13/2024 10:44:40 PM
[0:] No refreshment happened 3/13/2024 10:44:40 PM
...
I have updated the retryAttempt
to 15 seconds to give further time to retrieve a new refresh token:
public static AsyncRetryPolicy<HttpResponseMessage> AuthEnsuringPolicy = Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(resp => resp.StatusCode == HttpStatusCode.Unauthorized)
.WaitAndRetryAsync(3,
retryAttempt => TimeSpan.FromSeconds(15),
onRetry: async (resp, timeSpan, context) => _ = await App.ContainerProvider.Resolve<IAuthTokenService>().RefreshTokenAsync((HttpClient)context["httpClient"]).ConfigureAwait(true));
}
I could be wrong but from my testing it would seem that when setting httpClient.DefaultRequestHeaders.Add(Constants.AuthenticationTokenHeaderKey, securityToken);
on the RefreshTokenAsync
that subsequently calls do not have the updated token? Hence it attempts the login again but it should actually be completing the initial request as the token was refreshed.
from polly.
Sorry for the late response I was away from keyboard for several days.
I've tried to reproduce your problem with and without Polly.
Without Polly
private const string ClientName = "TestClient";
private const string HeaderKey = "AuthToken";
public static async Task Main()
{
var collection = new ServiceCollection();
collection.AddHttpClient(ClientName, (sp, client) =>
{
client.DefaultRequestHeaders.Add(HeaderKey, GetToken());
});
var provider = collection.BuildServiceProvider();
//Simulate retry
for(int i = 0; i < 5; i++)
{
Test(provider);
await Task.Delay(Random.Shared.Next(1000));
}
}
public static void Test(IServiceProvider sp)
{
var factory = sp.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient(ClientName);
var token = client.DefaultRequestHeaders.GetValues(HeaderKey).First();
Console.WriteLine($"Token: {token}");
//Simulate token refresh
client.DefaultRequestHeaders.Remove(HeaderKey);
client.DefaultRequestHeaders.Add(HeaderKey, GetToken());
}
private static string GetToken() => DateTime.UtcNow.TimeOfDay.ToString();
And it correctly updates the header. See the related dotnet fiddle.
With Polly
private const string ClientName = "TestClient";
private const string HeaderKey = "AuthToken";
private const string ContextKey = "HttpClient";
public static async Task Main()
{
var collection = new ServiceCollection();
collection.AddHttpClient(ClientName, (sp, client) =>
{
client.DefaultRequestHeaders.Add(HeaderKey, GetToken());
});
var provider = collection.BuildServiceProvider();
var context = new Polly.Context();
var factory = provider.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient(ClientName);
context[ContextKey] = client;
try
{
await GetRetry().ExecuteAsync(ctx => Test(), context);
}
catch(Exception)
{
Console.WriteLine("Final retry failed as well");
}
}
public static Task Test() => Task.FromException(new Exception("Damn"));
private static string GetToken() => DateTime.UtcNow.TimeOfDay.ToString();
public static IAsyncPolicy GetRetry() =>
Policy
.Handle<Exception>()
.WaitAndRetryAsync(4,
retryAttempt => TimeSpan.FromMilliseconds(Random.Shared.Next(1000)),
onRetry: (ex, ts, ctx) => {
var client = ctx[ContextKey] as HttpClient;
var token = client.DefaultRequestHeaders.GetValues(HeaderKey).First();
Console.WriteLine($"Token: {token}");
client.DefaultRequestHeaders.Remove(HeaderKey);
client.DefaultRequestHeaders.Add(HeaderKey, GetToken());
});
And it also correctly updates the header. See the related dotnet fiddle.
So, there must be something else which is not shared in this thread.
from polly.
@peter-csala again thank you for your continued assistance on this issue 🙏 hopefully the following can provide a more complete picture of what my code is doing but somethings have been deliberately omitted for brevity, I don't think there is anything outside these code blocks further I can add that would impact the ability to refresh the token for future calls:
App.xaml.cs
Sets up the HttpClient
with the Service Collection for DI registration including taking advantage of the Polly FallbackAsync
IAsyncPolicy<HttpResponseMessage> wrapOfRetryAndFallback =
Policy.WrapAsync(Policy.HandleResult<HttpResponseMessage>(r =>
!r.IsSuccessStatusCode).FallbackAsync(fallbackActionAsync, onFallBackAsync), PollyPolicies.AuthEnsuringPolicy);
_ = containerRegistry.RegisterSingleton<IHttpClientFactory>(() =>
new ServiceCollection().AddHttpClient(Constants.HttpClientWithRetry).AddPolicyHandler(wrapOfRetryAndFallback).Services.BuildServiceProvider().GetService<IHttpClientFactory>());
ApiService.cs
Following is where the initial request is called from such as LoginCustomerAsync
Example request to login customer
async Task<IdentityToken?> IApiService.LoginCustomerAsync(Login login)
{
StringContent stringContent = new(login.ToJson(), Encoding.UTF8, "application/json");
MemoryStream? result =
await executeRestRequestAsync(new Uri(string.Format("{0}/Access/Login?...."))), HttpMethod.Post, stringContent, requiresAuthenticationTokenHeader: false)
.ConfigureAwait(false);
if (result is not null)
{
DataContractJsonSerializer js = new(typeof(IdentityToken));
return (IdentityToken?)js.ReadObject(result);
}
throw new Exception("HTTP returned null");
}
Method that handles each request, this is where I retrieve the Security Token if it exists will be used as the header
& the Polly Context
takes the HttpClient
here as well:
private async Task<MemoryStream?> executeRestRequestAsync(Uri uri, HttpMethod httpMethod, StringContent? stringContent = null, bool requiresAuthenticationTokenHeader = true)
{
HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClientWithRetry);
// Only add security key if empty and not null (will be null on first login customer
// required on that request)
if (await _localStorageService.GetSecurityTokenAsync().ConfigureAwait(false) is string securityToken && !string.IsNullOrEmpty(securityToken) && requiresAuthenticationTokenHeader)
{
if (httpClient.DefaultRequestHeaders.Contains(Constants.AuthenticationTokenHeaderKey))
{
_ = httpClient.DefaultRequestHeaders.Remove(Constants.AuthenticationTokenHeaderKey);
}
httpClient.DefaultRequestHeaders.Add(Constants.AuthenticationTokenHeaderKey, securityToken);
}
HttpRequestMessage? requestMessage = null;
// Check the HTTP method enum to determine the HTTP method to call
switch (httpMethod)
{
case HttpMethod.Get:
requestMessage = new(System.Net.Http.HttpMethod.Get, uri);
break;
case HttpMethod.Post:
requestMessage = new(System.Net.Http.HttpMethod.Post, uri)
{
Content = stringContent
};
break;
case HttpMethod.Put:
requestMessage = new(System.Net.Http.HttpMethod.Put, uri)
{
Content = stringContent
};
break;
case HttpMethod.Delete:
requestMessage = new(System.Net.Http.HttpMethod.Delete, uri);
break;
}
requestMessage.SetPolicyExecutionContext(new Context
{
{ "retrycount", 2 },
{ "httpClient", httpClient }
});
HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(requestMessage).ConfigureAwait(true);
using Stream stream = await httpResponseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false);
using StreamReader steamReader = new(stream);
using JsonTextReader jsonTextReader = new(steamReader);
if (httpResponseMessage.IsSuccessStatusCode)
{
return new MemoryStream(buffer: stream.ToByteArray());
}
...
PollyPolicies.cs
The retry policy will take the HttpClient
from the context and some checks on the customer signed in status, if refresh token is needs refreshing then the HttpClient
will be passed to the RefreshTokenAsync
method to update the header
whilst also updating the locally stored security token
public static AsyncRetryPolicy<HttpResponseMessage> AuthEnsuringPolicy = Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(resp => resp.StatusCode == HttpStatusCode.Unauthorized && App.ContainerProvider.Resolve<ILocalStorageService>().GetStaySignedIn())
.WaitAndRetryAsync(3,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: async (resp, timeSpan, context) =>
{
if (context["httpClient"] is HttpClient httpClient
&& App.ContainerProvider.Resolve<ILocalStorageService>().GetCustomer() is Customer customer && !customer.IsLoggedIn
&& customer.StaySignedIn)
{
_ = await App.ContainerProvider.Resolve<IAuthTokenService>().RefreshTokenAsync(httpClient).ConfigureAwait(true);
}
});
AuthTokenService.cs
This appears to be working fine, the HttpClient
gets passed further down the line and updates only once. The RefreshAuthorizationTokenAsync
will re sign in a customer using the locally stored password and username to retrieve new token.
public class AuthTokenService : IAuthTokenService
{
private static readonly SemaphoreSlim _semaphoreSlim = new(1);
private static DateTime? _lastRefreshed;
public async Task<bool> RefreshTokenAsync(HttpClient httpClient)
{
await _semaphoreSlim.WaitAsync().ConfigureAwait(true);
try
{
if (!_lastRefreshed.HasValue)
{
await UpdateAuthenticationTokenHeaderAsync(httpClient).ConfigureAwait(true);
}
else if ((_lastRefreshed.Value - DateTime.UtcNow).Duration() < TimeSpan.FromSeconds(5).Duration())
{
Debug.WriteLine($"No refreshment happened {DateTime.UtcNow}");
return false;
}
else
{
await UpdateAuthenticationTokenHeaderAsync(httpClient).ConfigureAwait(true);
}
}
finally
{
_ = _semaphoreSlim.Release();
}
return false;
}
private static async Task UpdateAuthenticationTokenHeaderAsync(HttpClient httpClient)
{
ILocalStorageService localStorageService = App.ContainerProvider.Resolve<ILocalStorageService>();
if (localStorageService.GetCustomer() is Customer customer
&& await customer.GetPasswordAsync().ConfigureAwait(true) is string password
&& await App.ContainerProvider.Resolve<IUserManagementService>().RefreshAuthorizationTokenAsync(customer.Mobile, password).ConfigureAwait(true) is not null)
{
if (httpClient.DefaultRequestHeaders.Contains(Constants.AuthenticationTokenHeaderKey))
{
_ = httpClient.DefaultRequestHeaders.Remove(Constants.AuthenticationTokenHeaderKey);
}
if (await localStorageService.GetSecurityTokenAsync().ConfigureAwait(false) is string securityToken
&& !string.IsNullOrEmpty(securityToken))
{
httpClient.DefaultRequestHeaders.Add(Constants.AuthenticationTokenHeaderKey, securityToken);
}
Debug.WriteLine($"Refreshment happened {DateTime.UtcNow}");
_lastRefreshed = DateTime.UtcNow;
}
}
}
UserManagementService.cs
The end routine lays within the RefreshAuthorizationTokenAsync
method that should do the re-login for a customer and update the locally stored SecurityToken
that will be used for subsequent API calls. When this completes then in the previous code block the headers
will receive the new token that was persisted locally.
async Task<long?> IUserManagementService.RefreshAuthorizationTokenAsync(string mobileNumber, string password)
{
IdentityToken? identityToken = await Policy<IdentityToken?>.Handle<Exception>().FallbackAsync(async (outcome, context, ct) =>
{
...
}
return outcome.Result;
}, (ct, cx) => Task.CompletedTask).ExecuteAsync(async () => await _apiService.LoginCustomerAsync(new Login
{
Password = password,
Username = mobileNumber
}).ConfigureAwait(true)).ConfigureAwait(true);
if (identityToken is not null)
{
bool isCustomer = false;
string? identityId = string.Empty;
if (!identityToken.Claims.Any())
{
...
}
else
{
foreach (IdentityTokenClaim c in identityToken.Claims)
{
if (string.Compare(c.ClaimType, Constants.RoleClaim) == 0)
{
isCustomer = string.Compare(c.ClaimValue, "Customer", true) == 0;
}
if (string.Compare(c.ClaimType, Constants.NameIdentifierClaim) == 0)
{
identityId = c.ClaimValue;
}
}
if (isCustomer && identityId is not null && long.TryParse(identityId, out long customerId))
{
await _localStorageService.SetAuthTokenAsync(identityToken.SecurityToken).ConfigureAwait(true);
_localStorageService.SetSecurityTokenExpiry(identityToken.Expires);
return customerId;
}
else
{
...
}
}
}
return null;
}
Let me know if you can see something that might be causing issues but from the
from polly.
@LeoJHarris Could you please create a sample github repo with the above code fragments?
Then I could play with it on my machine to better understand the data and control flow.
UPDATE 1
I tried to extract relevant code fragments from your post. Mainly decorating the HttpClient
with the retry policy and passing the Context
object through the HttpRequestMessage
. Related dotnet fiddle.
public static async Task Main()
{
var collection = new ServiceCollection();
collection.AddHttpClient(ClientName, (sp, client) =>
{
client.DefaultRequestHeaders.Add(HeaderKey, GetToken());
})
.AddPolicyHandler(GetRetry());
var provider = collection.BuildServiceProvider();
var context = new Polly.Context();
var factory = provider.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient(ClientName);
context[ContextKey] = client;
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, "http://httpstat.us/500");
message.SetPolicyExecutionContext(context);
try
{
var response = await client.SendAsync(message);
response.EnsureSuccessStatusCode(); //throws HttpRequestException
}
catch(Exception)
{
Console.WriteLine("Final retry failed as well");
}
}
private static string GetToken() => DateTime.UtcNow.TimeOfDay.ToString();
private static IAsyncPolicy<HttpResponseMessage> GetRetry() =>
Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(res => !res.IsSuccessStatusCode)
.WaitAndRetryAsync(4,
retryAttempt => TimeSpan.FromMilliseconds(Random.Shared.Next(1000)),
onRetry: (ex, ts, ctx) => {
var client = ctx[ContextKey] as HttpClient;
var token = client.DefaultRequestHeaders.GetValues(HeaderKey).First();
Console.WriteLine($"Token: {token}");
client.DefaultRequestHeaders.Remove(HeaderKey);
client.DefaultRequestHeaders.Add(HeaderKey, GetToken());
});
It still works like a charm.
from polly.
@peter-csala this might take some time to get you a sample application, but Ill endeavor to get this ASAP. Keen to get this sorted.
Sure thing, no problem. Take your time.
from polly.
This issue is stale because it has been open for 60 days with no activity. It will be automatically closed in 14 days if no further updates are made.
from polly.
This issue was closed because it has been inactive for 14 days since being marked as stale.
from polly.
Related Issues (20)
- [Feature request]: Simplified and faster alternative to ExecuteOutcomeAsync HOT 7
- [Docs]: The main category pages are only available through the breadcrumbs HOT 5
- What's the origin of the name Polly? HOT 4
- [Bug]: DelayBackoffType Exponential Broken Sequence HOT 3
- [Bug]: Undocumented breaking changes from Policy.HandleInner to PredicateBuilder.HandleInner HOT 5
- [Feature request]: Resilience Event occured logs should be configurable as Warning unless it's the final attempt that failed HOT 11
- [Bug]: `GetRetryDelay` goes NEGATIVE after 1025 attempts [High Risk Issue] HOT 6
- [Question]: How to use a CircuitBreaker(basic or advanced) policy with multiple retry policies HOT 17
- [Cleanup the Polly codebase] Warning CA1062 HOT 1
- [Question]: How to get the HttpRequest in a callback supplied to stateful policy (e.g. circuit breaker) HOT 7
- [Feature request]: Simpler way to do basic ShouldHandle delegates for retry HOT 3
- TimeoutStrategyOptions.TimeoutGenerator docs are incorrect HOT 3
- [Feature request]: Generic version extending non-generic version HOT 2
- [Bug]: Unable to use `Polly.Core` in a plugin project in combination with `System.Text.Json` because of `Microsoft.Bcl.AsyncInterfaces` version mismatch HOT 2
- [Bug]: Timeout not always throwing TimeoutRejectedException HOT 3
- [Feature request]: Indicate time of possible circuit closure in `BrokenCircuitException` HOT 4
- Revert to xunit's default parallel algorithm in Polly.Core.Tests
- [Bug]: Expected value after metric, got "#" ("INVALID") while parsing: "Resilience_polly_str#" HOT 3
- [Feature request]: allow MaxRetryAttempts=0 HOT 3
- [Bug]: ResilienceProperties works incorrectly when the value requested by a key is null HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from polly.