Session execute Ensure on only single Task.

This commit is contained in:
2024-12-07 02:28:54 +01:00
parent 882cf0f604
commit 1bf5fc675e

View File

@@ -27,6 +27,8 @@ public sealed class Session
private TimeSpan _nextLoginBackoff;
private DateTime _nextValidTrial;
private Task<bool>? _ensureValidTask;
/// <summary>
/// Constructs a session from credentials and optionally a possibly live token.
/// </summary>
@@ -80,56 +82,72 @@ public sealed class Session
// Fast track positive check
return true;
if (await RefreshUserDataAsync(cancel) && (_isVip ?? false))
{
// Valid VIP (slower with API check)
_nextValidTrial = now + TokenValidPeriod;
return true;
}
// Serialize threads so we do not bounce the server too much
TaskCompletionSource<bool> task = new TaskCompletionSource<bool>();
Task<bool>? resultT = Interlocked.CompareExchange(ref _ensureValidTask, task.Task, null);
if (resultT != null)
// We are not the thread that will determine it
return await resultT;
if (now > _nextLoginTrial)
bool result = false;
try
{
// Try to log in again
string? token;
try
if (_token != null && await RefreshUserDataAsync(_token, cancel) && (_isVip ?? false))
{
string? salt;
if ((salt = await GetSaltAsync(_userName, cancel)) == null
|| (token = await GetTokenAsync(_userName, _password.HashPassword(salt), cancel)) == null)
// Valid VIP (slower with API check)
_nextValidTrial = now + TokenValidPeriod;
return result = true;
}
if (now > _nextLoginTrial)
{
// Try to log in again
string? token;
try
{
string? salt;
if ((salt = await GetSaltAsync(_userName, cancel)) == null
|| (token = await GetTokenAsync(_userName, _password.HashPassword(salt), cancel)) == null)
{
// Login failed
token = null;
}
}
catch
{
// Login failed
token = null;
}
}
catch
{
token = null;
if (token != null && await RefreshUserDataAsync(token, cancel) && (_isVip ?? false))
{
// Valid token with VIP
_nextLoginBackoff = InitialLoginBackoff;
_token = token;
_nextValidTrial = now + TokenValidPeriod;
return result = true;
}
else
{
// Invalid, no token or not VIP ...
TimeSpan nextBackoff = new TimeSpan(2 * _nextLoginBackoff.Ticks);
if (nextBackoff > MaxLoginBackoff)
nextBackoff = MaxLoginBackoff;
_nextLoginTrial = now + nextBackoff;
}
}
if (token != null && await RefreshUserDataAsync(cancel) && (_isVip ?? false))
{
// Valid token with VIP
_nextLoginBackoff = InitialLoginBackoff;
_token = token;
_nextValidTrial = now + TokenValidPeriod;
return true;
}
else
{
// Invalid, no token or not VIP ...
TimeSpan nextBackoff = new TimeSpan(2 * _nextLoginBackoff.Ticks);
if (nextBackoff > MaxLoginBackoff)
nextBackoff = MaxLoginBackoff;
_nextLoginTrial = now + nextBackoff;
}
// ... still invalid, no token or not VIP
_token = null;
_isVip = null;
_vipDaysLeft = null;
return result = false;
}
finally
{
task.SetResult(result);
GC.KeepAlive(Interlocked.Exchange(ref _ensureValidTask, null));
}
// ... still invalid, no token or not VIP
_token = null;
_isVip = null;
_vipDaysLeft = null;
return false;
}
/// <summary>
@@ -226,7 +244,7 @@ public sealed class Session
/// Gets user data and indirectly validates the session token.
/// </summary>
/// <returns>True if refresh has been successful and session token is valid. -or- False otherwise.</returns>
private async Task<bool> RefreshUserDataAsync(CancellationToken cancel)
private async Task<bool> RefreshUserDataAsync(string? token, CancellationToken cancel)
{
_isVip = null;
_vipDaysLeft = null;
@@ -234,7 +252,7 @@ public sealed class Session
// Example response: <?xml version="1.0" encoding="UTF-8"?><response><status>OK</status>...??...<app_version>30</app_version></response>
Uri dataUri = new Uri(WebshareApiUri, "user_data/");
HttpResponseMessage dataRes = await Program._http.PostAsync(dataUri, new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>(TokenKeyName, _token ?? "")
new KeyValuePair<string, string>(TokenKeyName, token ?? "")
}), cancel
);
if (!dataRes.IsSuccessStatusCode)