feat: upgrade to .NET 6, refactor everything

This commit is contained in:
spiral
2021-11-26 21:10:56 -05:00
parent d28e99ba43
commit 1918c56937
314 changed files with 27954 additions and 27966 deletions

View File

@@ -1,173 +1,172 @@
using System;
using System.Threading;
using Serilog;
namespace Myriad.Rest.Ratelimit
namespace Myriad.Rest.Ratelimit;
public class Bucket
{
public class Bucket
private static readonly TimeSpan Epsilon = TimeSpan.FromMilliseconds(10);
private static readonly TimeSpan FallbackDelay = TimeSpan.FromMilliseconds(200);
private static readonly TimeSpan StaleTimeout = TimeSpan.FromSeconds(5);
private readonly ILogger _logger;
private readonly SemaphoreSlim _semaphore = new(1, 1);
private bool _hasReceivedHeaders;
private DateTimeOffset? _nextReset;
private bool _resetTimeValid;
public Bucket(ILogger logger, string key, ulong major, int limit)
{
private static readonly TimeSpan Epsilon = TimeSpan.FromMilliseconds(10);
private static readonly TimeSpan FallbackDelay = TimeSpan.FromMilliseconds(200);
_logger = logger.ForContext<Bucket>();
private static readonly TimeSpan StaleTimeout = TimeSpan.FromSeconds(5);
Key = key;
Major = major;
private readonly ILogger _logger;
private readonly SemaphoreSlim _semaphore = new(1, 1);
Limit = limit;
Remaining = limit;
_resetTimeValid = false;
}
private DateTimeOffset? _nextReset;
private bool _resetTimeValid;
private bool _hasReceivedHeaders;
public string Key { get; }
public ulong Major { get; }
public Bucket(ILogger logger, string key, ulong major, int limit)
public int Remaining { get; private set; }
public int Limit { get; private set; }
public DateTimeOffset LastUsed { get; private set; } = DateTimeOffset.UtcNow;
public bool TryAcquire()
{
LastUsed = DateTimeOffset.Now;
try
{
_logger = logger.ForContext<Bucket>();
_semaphore.Wait();
Key = key;
Major = major;
Limit = limit;
Remaining = limit;
_resetTimeValid = false;
}
public string Key { get; }
public ulong Major { get; }
public int Remaining { get; private set; }
public int Limit { get; private set; }
public DateTimeOffset LastUsed { get; private set; } = DateTimeOffset.UtcNow;
public bool TryAcquire()
{
LastUsed = DateTimeOffset.Now;
try
if (Remaining > 0)
{
_semaphore.Wait();
if (Remaining > 0)
{
_logger.Verbose(
"{BucketKey}/{BucketMajor}: Bucket has [{BucketRemaining}/{BucketLimit} left], allowing through",
Key, Major, Remaining, Limit);
Remaining--;
return true;
}
_logger.Debug("{BucketKey}/{BucketMajor}: Bucket has [{BucketRemaining}/{BucketLimit}] left, denying",
_logger.Verbose(
"{BucketKey}/{BucketMajor}: Bucket has [{BucketRemaining}/{BucketLimit} left], allowing through",
Key, Major, Remaining, Limit);
return false;
}
finally
{
_semaphore.Release();
Remaining--;
return true;
}
_logger.Debug("{BucketKey}/{BucketMajor}: Bucket has [{BucketRemaining}/{BucketLimit}] left, denying",
Key, Major, Remaining, Limit);
return false;
}
public void HandleResponse(RatelimitHeaders headers)
finally
{
try
{
_semaphore.Wait();
_logger.Verbose("{BucketKey}/{BucketMajor}: Received rate limit headers: {@RateLimitHeaders}",
Key, Major, headers);
if (headers.ResetAfter != null)
{
var headerNextReset = DateTimeOffset.UtcNow + headers.ResetAfter.Value; // todo: server time
if (_nextReset == null || headerNextReset > _nextReset)
{
_logger.Verbose("{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server (after: {NextResetAfter}, remaining: {Remaining}, local remaining: {LocalRemaining})",
Key, Major, headerNextReset, headers.ResetAfter.Value, headers.Remaining, Remaining);
_nextReset = headerNextReset;
_resetTimeValid = true;
}
}
if (headers.Limit != null)
Limit = headers.Limit.Value;
if (headers.Remaining != null && !_hasReceivedHeaders)
{
var oldRemaining = Remaining;
Remaining = Math.Min(headers.Remaining.Value, Remaining);
_logger.Debug("{BucketKey}/{BucketMajor}: Received first remaining of {HeaderRemaining}, previous local remaining is {LocalRemaining}, new local remaining is {Remaining}",
Key, Major, headers.Remaining.Value, oldRemaining, Remaining);
_hasReceivedHeaders = true;
}
}
finally
{
_semaphore.Release();
}
}
public void Tick(DateTimeOffset now)
{
try
{
_semaphore.Wait();
// If we don't have any reset data, "snap" it to now
// This happens before first request and at this point the reset is invalid anyway, so it's fine
// but it ensures the stale timeout doesn't trigger early by using `default` value
if (_nextReset == null)
_nextReset = now;
// If we're past the reset time *and* we haven't reset already, do that
var timeSinceReset = now - _nextReset;
var shouldReset = _resetTimeValid && timeSinceReset > TimeSpan.Zero;
if (shouldReset)
{
_logger.Debug("{BucketKey}/{BucketMajor}: Bucket timed out, refreshing with {BucketLimit} requests",
Key, Major, Limit);
Remaining = Limit;
_resetTimeValid = false;
return;
}
// We've run out of requests without having any new reset time,
// *and* it's been longer than a set amount - add one request back to the pool and hope that one returns
var isBucketStale = !_resetTimeValid && Remaining <= 0 && timeSinceReset > StaleTimeout;
if (isBucketStale)
{
_logger.Warning(
"{BucketKey}/{BucketMajor}: Bucket is stale ({StaleTimeout} passed with no rate limit info), allowing one request through",
Key, Major, StaleTimeout);
Remaining = 1;
// Reset the (still-invalid) reset time to now, so we don't keep hitting this conditional over and over...
_nextReset = now;
}
}
finally
{
_semaphore.Release();
}
}
public TimeSpan GetResetDelay(DateTimeOffset now)
{
// If we don't have a valid reset time, return the fallback delay always
// (so it'll keep spinning until we hopefully have one...)
if (!_resetTimeValid)
return FallbackDelay;
var delay = (_nextReset ?? now) - now;
// If we have a really small (or negative) value, return a fallback delay too
if (delay < Epsilon)
return FallbackDelay;
return delay;
_semaphore.Release();
}
}
public void HandleResponse(RatelimitHeaders headers)
{
try
{
_semaphore.Wait();
_logger.Verbose("{BucketKey}/{BucketMajor}: Received rate limit headers: {@RateLimitHeaders}",
Key, Major, headers);
if (headers.ResetAfter != null)
{
var headerNextReset = DateTimeOffset.UtcNow + headers.ResetAfter.Value; // todo: server time
if (_nextReset == null || headerNextReset > _nextReset)
{
_logger.Verbose(
"{BucketKey}/{BucketMajor}: Received reset time {NextReset} from server (after: {NextResetAfter}, remaining: {Remaining}, local remaining: {LocalRemaining})",
Key, Major, headerNextReset, headers.ResetAfter.Value, headers.Remaining, Remaining
);
_nextReset = headerNextReset;
_resetTimeValid = true;
}
}
if (headers.Limit != null)
Limit = headers.Limit.Value;
if (headers.Remaining != null && !_hasReceivedHeaders)
{
var oldRemaining = Remaining;
Remaining = Math.Min(headers.Remaining.Value, Remaining);
_logger.Debug(
"{BucketKey}/{BucketMajor}: Received first remaining of {HeaderRemaining}, previous local remaining is {LocalRemaining}, new local remaining is {Remaining}",
Key, Major, headers.Remaining.Value, oldRemaining, Remaining);
_hasReceivedHeaders = true;
}
}
finally
{
_semaphore.Release();
}
}
public void Tick(DateTimeOffset now)
{
try
{
_semaphore.Wait();
// If we don't have any reset data, "snap" it to now
// This happens before first request and at this point the reset is invalid anyway, so it's fine
// but it ensures the stale timeout doesn't trigger early by using `default` value
if (_nextReset == null)
_nextReset = now;
// If we're past the reset time *and* we haven't reset already, do that
var timeSinceReset = now - _nextReset;
var shouldReset = _resetTimeValid && timeSinceReset > TimeSpan.Zero;
if (shouldReset)
{
_logger.Debug("{BucketKey}/{BucketMajor}: Bucket timed out, refreshing with {BucketLimit} requests",
Key, Major, Limit);
Remaining = Limit;
_resetTimeValid = false;
return;
}
// We've run out of requests without having any new reset time,
// *and* it's been longer than a set amount - add one request back to the pool and hope that one returns
var isBucketStale = !_resetTimeValid && Remaining <= 0 && timeSinceReset > StaleTimeout;
if (isBucketStale)
{
_logger.Warning(
"{BucketKey}/{BucketMajor}: Bucket is stale ({StaleTimeout} passed with no rate limit info), allowing one request through",
Key, Major, StaleTimeout);
Remaining = 1;
// Reset the (still-invalid) reset time to now, so we don't keep hitting this conditional over and over...
_nextReset = now;
}
}
finally
{
_semaphore.Release();
}
}
public TimeSpan GetResetDelay(DateTimeOffset now)
{
// If we don't have a valid reset time, return the fallback delay always
// (so it'll keep spinning until we hopefully have one...)
if (!_resetTimeValid)
return FallbackDelay;
var delay = (_nextReset ?? now) - now;
// If we have a really small (or negative) value, return a fallback delay too
if (delay < Epsilon)
return FallbackDelay;
return delay;
}
}

View File

@@ -1,82 +1,79 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
namespace Myriad.Rest.Ratelimit
namespace Myriad.Rest.Ratelimit;
public class BucketManager: IDisposable
{
public class BucketManager: IDisposable
private static readonly TimeSpan StaleBucketTimeout = TimeSpan.FromMinutes(5);
private static readonly TimeSpan PruneWorkerInterval = TimeSpan.FromMinutes(1);
private readonly ConcurrentDictionary<(string key, ulong major), Bucket> _buckets = new();
private readonly ConcurrentDictionary<string, string> _endpointKeyMap = new();
private readonly ConcurrentDictionary<string, int> _knownKeyLimits = new();
private readonly ILogger _logger;
private readonly Task _worker;
private readonly CancellationTokenSource _workerCts = new();
public BucketManager(ILogger logger)
{
private static readonly TimeSpan StaleBucketTimeout = TimeSpan.FromMinutes(5);
private static readonly TimeSpan PruneWorkerInterval = TimeSpan.FromMinutes(1);
private readonly ConcurrentDictionary<(string key, ulong major), Bucket> _buckets = new();
_logger = logger.ForContext<BucketManager>();
_worker = PruneWorker(_workerCts.Token);
}
private readonly ConcurrentDictionary<string, string> _endpointKeyMap = new();
private readonly ConcurrentDictionary<string, int> _knownKeyLimits = new();
public void Dispose()
{
_workerCts.Dispose();
_worker.Dispose();
}
private readonly ILogger _logger;
public Bucket? GetBucket(string endpoint, ulong major)
{
if (!_endpointKeyMap.TryGetValue(endpoint, out var key))
return null;
private readonly Task _worker;
private readonly CancellationTokenSource _workerCts = new();
if (_buckets.TryGetValue((key, major), out var bucket))
return bucket;
public BucketManager(ILogger logger)
if (!_knownKeyLimits.TryGetValue(key, out var knownLimit))
return null;
_logger.Debug("Creating new bucket {BucketKey}/{BucketMajor} with limit {KnownLimit}", key, major,
knownLimit);
return _buckets.GetOrAdd((key, major),
k => new Bucket(_logger, k.Item1, k.Item2, knownLimit));
}
public void UpdateEndpointInfo(string endpoint, string key, int? limit)
{
_endpointKeyMap[endpoint] = key;
if (limit != null)
_knownKeyLimits[key] = limit.Value;
}
private async Task PruneWorker(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
_logger = logger.ForContext<BucketManager>();
_worker = PruneWorker(_workerCts.Token);
await Task.Delay(PruneWorkerInterval, ct);
PruneStaleBuckets(DateTimeOffset.UtcNow);
}
}
public void Dispose()
private void PruneStaleBuckets(DateTimeOffset now)
{
foreach (var (key, bucket) in _buckets)
{
_workerCts.Dispose();
_worker.Dispose();
}
if (now - bucket.LastUsed <= StaleBucketTimeout)
continue;
public Bucket? GetBucket(string endpoint, ulong major)
{
if (!_endpointKeyMap.TryGetValue(endpoint, out var key))
return null;
if (_buckets.TryGetValue((key, major), out var bucket))
return bucket;
if (!_knownKeyLimits.TryGetValue(key, out var knownLimit))
return null;
_logger.Debug("Creating new bucket {BucketKey}/{BucketMajor} with limit {KnownLimit}", key, major, knownLimit);
return _buckets.GetOrAdd((key, major),
k => new Bucket(_logger, k.Item1, k.Item2, knownLimit));
}
public void UpdateEndpointInfo(string endpoint, string key, int? limit)
{
_endpointKeyMap[endpoint] = key;
if (limit != null)
_knownKeyLimits[key] = limit.Value;
}
private async Task PruneWorker(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(PruneWorkerInterval, ct);
PruneStaleBuckets(DateTimeOffset.UtcNow);
}
}
private void PruneStaleBuckets(DateTimeOffset now)
{
foreach (var (key, bucket) in _buckets)
{
if (now - bucket.LastUsed <= StaleBucketTimeout)
continue;
_logger.Debug("Pruning unused bucket {BucketKey}/{BucketMajor} (last used at {BucketLastUsed})",
bucket.Key, bucket.Major, bucket.LastUsed);
_buckets.TryRemove(key, out _);
}
_logger.Debug("Pruning unused bucket {BucketKey}/{BucketMajor} (last used at {BucketLastUsed})",
bucket.Key, bucket.Major, bucket.LastUsed);
_buckets.TryRemove(key, out _);
}
}
}

View File

@@ -1,46 +1,40 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Polly;
namespace Myriad.Rest.Ratelimit
namespace Myriad.Rest.Ratelimit;
public class DiscordRateLimitPolicy: AsyncPolicy<HttpResponseMessage>
{
public class DiscordRateLimitPolicy: AsyncPolicy<HttpResponseMessage>
public const string EndpointContextKey = "Endpoint";
public const string MajorContextKey = "Major";
private readonly Ratelimiter _ratelimiter;
public DiscordRateLimitPolicy(Ratelimiter ratelimiter, PolicyBuilder<HttpResponseMessage>? policyBuilder = null)
: base(policyBuilder)
{
public const string EndpointContextKey = "Endpoint";
public const string MajorContextKey = "Major";
_ratelimiter = ratelimiter;
}
private readonly Ratelimiter _ratelimiter;
protected override async Task<HttpResponseMessage> ImplementationAsync(
Func<Context, CancellationToken, Task<HttpResponseMessage>> action, Context context, CancellationToken ct,
bool continueOnCapturedContext)
{
if (!context.TryGetValue(EndpointContextKey, out var endpointObj) || !(endpointObj is string endpoint))
throw new ArgumentException("Must provide endpoint in Polly context");
public DiscordRateLimitPolicy(Ratelimiter ratelimiter, PolicyBuilder<HttpResponseMessage>? policyBuilder = null)
: base(policyBuilder)
{
_ratelimiter = ratelimiter;
}
if (!context.TryGetValue(MajorContextKey, out var majorObj) || !(majorObj is ulong major))
throw new ArgumentException("Must provide major in Polly context");
protected override async Task<HttpResponseMessage> ImplementationAsync(
Func<Context, CancellationToken, Task<HttpResponseMessage>> action, Context context, CancellationToken ct,
bool continueOnCapturedContext)
{
if (!context.TryGetValue(EndpointContextKey, out var endpointObj) || !(endpointObj is string endpoint))
throw new ArgumentException("Must provide endpoint in Polly context");
// Check rate limit, throw if we're not allowed...
_ratelimiter.AllowRequestOrThrow(endpoint, major, DateTimeOffset.Now);
if (!context.TryGetValue(MajorContextKey, out var majorObj) || !(majorObj is ulong major))
throw new ArgumentException("Must provide major in Polly context");
// We're OK, push it through
var response = await action(context, ct).ConfigureAwait(continueOnCapturedContext);
// Check rate limit, throw if we're not allowed...
_ratelimiter.AllowRequestOrThrow(endpoint, major, DateTimeOffset.Now);
// Update rate limit state with headers
var headers = RatelimitHeaders.Parse(response);
_ratelimiter.HandleResponse(headers, endpoint, major);
// We're OK, push it through
var response = await action(context, ct).ConfigureAwait(continueOnCapturedContext);
// Update rate limit state with headers
var headers = RatelimitHeaders.Parse(response);
_ratelimiter.HandleResponse(headers, endpoint, major);
return response;
}
return response;
}
}

View File

@@ -1,85 +1,79 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
namespace Myriad.Rest.Ratelimit
namespace Myriad.Rest.Ratelimit;
public record RatelimitHeaders
{
public record RatelimitHeaders
private const string LimitHeader = "X-RateLimit-Limit";
private const string RemainingHeader = "X-RateLimit-Remaining";
private const string ResetHeader = "X-RateLimit-Reset";
private const string ResetAfterHeader = "X-RateLimit-Reset-After";
private const string BucketHeader = "X-RateLimit-Bucket";
private const string GlobalHeader = "X-RateLimit-Global";
public bool Global { get; private set; }
public int? Limit { get; private set; }
public int? Remaining { get; private set; }
public DateTimeOffset? Reset { get; private set; }
public TimeSpan? ResetAfter { get; private set; }
public string? Bucket { get; private set; }
public DateTimeOffset? ServerDate { get; private set; }
public bool HasRatelimitInfo =>
Limit != null && Remaining != null && Reset != null && ResetAfter != null && Bucket != null;
public static RatelimitHeaders Parse(HttpResponseMessage response)
{
private const string LimitHeader = "X-RateLimit-Limit";
private const string RemainingHeader = "X-RateLimit-Remaining";
private const string ResetHeader = "X-RateLimit-Reset";
private const string ResetAfterHeader = "X-RateLimit-Reset-After";
private const string BucketHeader = "X-RateLimit-Bucket";
private const string GlobalHeader = "X-RateLimit-Global";
public bool Global { get; private set; }
public int? Limit { get; private set; }
public int? Remaining { get; private set; }
public DateTimeOffset? Reset { get; private set; }
public TimeSpan? ResetAfter { get; private set; }
public string? Bucket { get; private set; }
public DateTimeOffset? ServerDate { get; private set; }
public bool HasRatelimitInfo =>
Limit != null && Remaining != null && Reset != null && ResetAfter != null && Bucket != null;
public RatelimitHeaders() { }
public static RatelimitHeaders Parse(HttpResponseMessage response)
var headers = new RatelimitHeaders
{
var headers = new RatelimitHeaders
{
ServerDate = response.Headers.Date,
Limit = TryGetInt(response, LimitHeader),
Remaining = TryGetInt(response, RemainingHeader),
Bucket = TryGetHeader(response, BucketHeader)
};
ServerDate = response.Headers.Date,
Limit = TryGetInt(response, LimitHeader),
Remaining = TryGetInt(response, RemainingHeader),
Bucket = TryGetHeader(response, BucketHeader)
};
var resetTimestamp = TryGetDouble(response, ResetHeader);
if (resetTimestamp != null)
headers.Reset = DateTimeOffset.FromUnixTimeMilliseconds((long)(resetTimestamp.Value * 1000));
var resetTimestamp = TryGetDouble(response, ResetHeader);
if (resetTimestamp != null)
headers.Reset = DateTimeOffset.FromUnixTimeMilliseconds((long)(resetTimestamp.Value * 1000));
var resetAfterSeconds = TryGetDouble(response, ResetAfterHeader);
if (resetAfterSeconds != null)
headers.ResetAfter = TimeSpan.FromSeconds(resetAfterSeconds.Value);
var resetAfterSeconds = TryGetDouble(response, ResetAfterHeader);
if (resetAfterSeconds != null)
headers.ResetAfter = TimeSpan.FromSeconds(resetAfterSeconds.Value);
var global = TryGetHeader(response, GlobalHeader);
if (global != null && bool.TryParse(global, out var globalBool))
headers.Global = globalBool;
var global = TryGetHeader(response, GlobalHeader);
if (global != null && bool.TryParse(global, out var globalBool))
headers.Global = globalBool;
return headers;
}
return headers;
}
private static string? TryGetHeader(HttpResponseMessage response, string headerName)
{
if (!response.Headers.TryGetValues(headerName, out var values))
return null;
private static string? TryGetHeader(HttpResponseMessage response, string headerName)
{
if (!response.Headers.TryGetValues(headerName, out var values))
return null;
return values.FirstOrDefault();
}
return values.FirstOrDefault();
}
private static int? TryGetInt(HttpResponseMessage response, string headerName)
{
var valueString = TryGetHeader(response, headerName);
private static int? TryGetInt(HttpResponseMessage response, string headerName)
{
var valueString = TryGetHeader(response, headerName);
if (!int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
return null;
if (!int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
return null;
return value;
}
return value;
}
private static double? TryGetDouble(HttpResponseMessage response, string headerName)
{
var valueString = TryGetHeader(response, headerName);
private static double? TryGetDouble(HttpResponseMessage response, string headerName)
{
var valueString = TryGetHeader(response, headerName);
if (!double.TryParse(valueString, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
return null;
if (!double.TryParse(valueString, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
return null;
return value;
}
return value;
}
}

View File

@@ -1,86 +1,83 @@
using System;
using Myriad.Rest.Exceptions;
using Serilog;
namespace Myriad.Rest.Ratelimit
namespace Myriad.Rest.Ratelimit;
public class Ratelimiter: IDisposable
{
public class Ratelimiter: IDisposable
private readonly BucketManager _buckets;
private readonly ILogger _logger;
private DateTimeOffset? _globalRateLimitExpiry;
public Ratelimiter(ILogger logger)
{
private readonly BucketManager _buckets;
private readonly ILogger _logger;
_logger = logger.ForContext<Ratelimiter>();
_buckets = new BucketManager(logger);
}
private DateTimeOffset? _globalRateLimitExpiry;
public void Dispose()
{
_buckets.Dispose();
}
public Ratelimiter(ILogger logger)
public void AllowRequestOrThrow(string endpoint, ulong major, DateTimeOffset now)
{
if (IsGloballyRateLimited(now))
{
_logger = logger.ForContext<Ratelimiter>();
_buckets = new BucketManager(logger);
_logger.Warning("Globally rate limited until {GlobalRateLimitExpiry}, cancelling request",
_globalRateLimitExpiry);
throw new GloballyRatelimitedException();
}
public void Dispose()
var bucket = _buckets.GetBucket(endpoint, major);
if (bucket == null)
{
_buckets.Dispose();
// No rate limit for this endpoint (yet), allow through
_logger.Debug("No rate limit data for endpoint {Endpoint}, allowing through", endpoint);
return;
}
public void AllowRequestOrThrow(string endpoint, ulong major, DateTimeOffset now)
bucket.Tick(now);
if (bucket.TryAcquire())
// We're allowed to send it! :)
return;
// We can't send this request right now; retrying...
var waitTime = bucket.GetResetDelay(now);
// add a small buffer for Timing:tm:
waitTime += TimeSpan.FromMilliseconds(50);
// (this is caught by a WaitAndRetry Polly handler, if configured)
throw new RatelimitBucketExhaustedException(bucket, waitTime);
}
public void HandleResponse(RatelimitHeaders headers, string endpoint, ulong major)
{
if (!headers.HasRatelimitInfo)
return;
// TODO: properly calculate server time?
if (headers.Global)
{
if (IsGloballyRateLimited(now))
{
_logger.Warning("Globally rate limited until {GlobalRateLimitExpiry}, cancelling request",
_globalRateLimitExpiry);
throw new GloballyRatelimitedException();
}
_logger.Warning(
"Global rate limit hit, resetting at {GlobalRateLimitExpiry} (in {GlobalRateLimitResetAfter}!",
_globalRateLimitExpiry, headers.ResetAfter);
_globalRateLimitExpiry = headers.Reset;
}
else
{
// Update buckets first, then get it again, to properly "transfer" this info over to the new value
_buckets.UpdateEndpointInfo(endpoint, headers.Bucket!, headers.Limit);
var bucket = _buckets.GetBucket(endpoint, major);
if (bucket == null)
{
// No rate limit for this endpoint (yet), allow through
_logger.Debug("No rate limit data for endpoint {Endpoint}, allowing through", endpoint);
return;
}
bucket.Tick(now);
if (bucket.TryAcquire())
// We're allowed to send it! :)
return;
// We can't send this request right now; retrying...
var waitTime = bucket.GetResetDelay(now);
// add a small buffer for Timing:tm:
waitTime += TimeSpan.FromMilliseconds(50);
// (this is caught by a WaitAndRetry Polly handler, if configured)
throw new RatelimitBucketExhaustedException(bucket, waitTime);
bucket?.HandleResponse(headers);
}
public void HandleResponse(RatelimitHeaders headers, string endpoint, ulong major)
{
if (!headers.HasRatelimitInfo)
return;
// TODO: properly calculate server time?
if (headers.Global)
{
_logger.Warning(
"Global rate limit hit, resetting at {GlobalRateLimitExpiry} (in {GlobalRateLimitResetAfter}!",
_globalRateLimitExpiry, headers.ResetAfter);
_globalRateLimitExpiry = headers.Reset;
}
else
{
// Update buckets first, then get it again, to properly "transfer" this info over to the new value
_buckets.UpdateEndpointInfo(endpoint, headers.Bucket!, headers.Limit);
var bucket = _buckets.GetBucket(endpoint, major);
bucket?.HandleResponse(headers);
}
}
private bool IsGloballyRateLimited(DateTimeOffset now) =>
_globalRateLimitExpiry > now;
}
private bool IsGloballyRateLimited(DateTimeOffset now) =>
_globalRateLimitExpiry > now;
}