@@ -10,52 +10,83 @@ namespace Discord.Net.Queue
{
{
public class RequestQueue
public class RequestQueue
{
{
private readonly static ImmutableDictionary<GlobalBucket, BucketDefinition> _globalLimits;
private readonly static ImmutableDictionary<GuildBucket, BucketDefinition> _guildLimits;
public event Func<string, Bucket, int, Task> RateLimitTriggered;
private readonly static ImmutableDictionary<GlobalBucket, Bucket> _globalLimits;
private readonly static ImmutableDictionary<GuildBucket, Bucket> _guildLimits;
private readonly static ImmutableDictionary<ChannelBucket, Bucket> _channelLimits;
private readonly SemaphoreSlim _lock;
private readonly SemaphoreSlim _lock;
private readonly RequestQueueBucket[] _globalBuckets;
private readonly RequestQueueBucket[] _globalBuckets;
private readonly ConcurrentDictionary<ulong, RequestQueueBucket>[] _guildBuckets;
private readonly ConcurrentDictionary<ulong, RequestQueueBucket>[] _guildBuckets;
private readonly ConcurrentDictionary<ulong, RequestQueueBucket>[] _channelBuckets;
private CancellationTokenSource _clearToken;
private CancellationTokenSource _clearToken;
private CancellationToken _parentToken;
private CancellationToken _parentToken;
private CancellationToken _cancelToken;
private CancellationToken _cancelToken;
static RequestQueue()
static RequestQueue()
{
{
_globalLimits = new Dictionary<GlobalBucket, BucketDefinition >
_globalLimits = new Dictionary<GlobalBucket, Bucket>
{
{
//REST
//REST
[GlobalBucket.GeneralRest] = new BucketDefinition(0, 0 ), //No Limit
[GlobalBucket.GeneralRest] = new Bucket(null, "rest", 0, 0, BucketTarget.Both ), //No Limit
//[GlobalBucket.Login] = new BucketDefinition(1, 1),
//[GlobalBucket.Login] = new BucketDefinition(1, 1),
[GlobalBucket.DirectMessage] = new BucketDefinition(5, 5 ),
[GlobalBucket.SendEditMessage] = new BucketDefinition(50, 10 ),
[GlobalBucket.DirectMessage] = new Bucket("bot:msg:dm", 5, 5, BucketTarget.Bot ),
[GlobalBucket.SendEditMessage] = new Bucket("bot:msg:global", 50, 10, BucketTarget.Bot ),
//Gateway
//Gateway
[GlobalBucket.GeneralGateway] = new BucketDefinition(120, 60),
[GlobalBucket.UpdateStatus] = new BucketDefinition(5, 1, GlobalBucket.GeneralGateway)
[GlobalBucket.GeneralGateway] = new Bucket(null, "gateway", 120, 60, BucketTarget.Both),
[GlobalBucket.UpdateStatus] = new Bucket(null, "status", 5, 1, BucketTarget.Both, GlobalBucket.GeneralGateway)
}.ToImmutableDictionary();
_guildLimits = new Dictionary<GuildBucket, Bucket>
{
//REST
[GuildBucket.SendEditMessage] = new Bucket("bot:msg:server", 5, 5, BucketTarget.Bot, GlobalBucket.SendEditMessage),
[GuildBucket.DeleteMessage] = new Bucket("dmsg", 5, 1, BucketTarget.Bot),
[GuildBucket.DeleteMessages] = new Bucket("bdmsg", 1, 1, BucketTarget.Bot),
[GuildBucket.ModifyMember] = new Bucket("guild_member", 10, 10, BucketTarget.Bot),
[GuildBucket.Nickname] = new Bucket("guild_member_nick", 1, 1, BucketTarget.Bot)
}.ToImmutableDictionary();
}.ToImmutableDictionary();
_guildLimits = new Dictionary<GuildBucket, BucketDefinition>
//Client-Only
_channelLimits = new Dictionary<ChannelBucket, Bucket>
{
{
//REST
//REST
[GuildBucket.SendEditMessage] = new BucketDefinition(5, 5, GlobalBucket.SendEditMessage),
[GuildBucket.DeleteMessage] = new BucketDefinition(5, 1),
[GuildBucket.DeleteMessages] = new BucketDefinition(1, 1),
[GuildBucket.ModifyMember] = new BucketDefinition(10, 10),
[GuildBucket.Nickname] = new BucketDefinition(1, 1)
[ChannelBucket.SendEditMessage] = new Bucket("msg", 10, 10, BucketTarget.Client, GlobalBucket.SendEditMessage),
}.ToImmutableDictionary();
}.ToImmutableDictionary();
}
}
public static Bucket GetBucketInfo(GlobalBucket bucket) => _globalLimits[bucket];
public static Bucket GetBucketInfo(GuildBucket bucket) => _guildLimits[bucket];
public static Bucket GetBucketInfo(ChannelBucket bucket) => _channelLimits[bucket];
public RequestQueue()
public RequestQueue()
{
{
_lock = new SemaphoreSlim(1, 1);
_lock = new SemaphoreSlim(1, 1);
_globalBuckets = new RequestQueueBucket[_globalLimits.Count];
_globalBuckets = new RequestQueueBucket[_globalLimits.Count];
foreach (var pair in _globalLimits)
foreach (var pair in _globalLimits)
_globalBuckets[(int)pair.Key] = CreateBucket(pair.Value);
{
//var target = _globalLimits[pair.Key].Target;
//if (target == BucketTarget.Both || (target == BucketTarget.Bot && isBot) || (target == BucketTarget.Client && !isBot))
_globalBuckets[(int)pair.Key] = CreateBucket(pair.Value);
}
_guildBuckets = new ConcurrentDictionary<ulong, RequestQueueBucket>[_guildLimits.Count];
_guildBuckets = new ConcurrentDictionary<ulong, RequestQueueBucket>[_guildLimits.Count];
for (int i = 0; i < _guildLimits.Count; i++)
for (int i = 0; i < _guildLimits.Count; i++)
_guildBuckets[i] = new ConcurrentDictionary<ulong, RequestQueueBucket>();
{
//var target = _guildLimits[(GuildBucket)i].Target;
//if (target == BucketTarget.Both || (target == BucketTarget.Bot && isBot) || (target == BucketTarget.Client && !isBot))
_guildBuckets[i] = new ConcurrentDictionary<ulong, RequestQueueBucket>();
}
_channelBuckets = new ConcurrentDictionary<ulong, RequestQueueBucket>[_channelLimits.Count];
for (int i = 0; i < _channelLimits.Count; i++)
{
//var target = _channelLimits[(GuildBucket)i].Target;
//if (target == BucketTarget.Both || (target == BucketTarget.Bot && isBot) || (target == BucketTarget.Client && !isBot))
_channelBuckets[i] = new ConcurrentDictionary<ulong, RequestQueueBucket>();
}
_clearToken = new CancellationTokenSource();
_clearToken = new CancellationTokenSource();
_cancelToken = CancellationToken.None;
_cancelToken = CancellationToken.None;
@@ -72,23 +103,23 @@ namespace Discord.Net.Queue
finally { _lock.Release(); }
finally { _lock.Release(); }
}
}
internal async Task<Stream> SendAsync(RestRequest request, BucketGroup group, int bucketId, ulong guild Id)
internal async Task<Stream> SendAsync(RestRequest request, BucketGroup group, int bucketId, ulong obj Id)
{
{
request.CancelToken = _cancelToken;
request.CancelToken = _cancelToken;
var bucket = GetBucket(group, bucketId, guild Id);
var bucket = GetBucket(group, bucketId, obj Id);
return await bucket.SendAsync(request).ConfigureAwait(false);
return await bucket.SendAsync(request).ConfigureAwait(false);
}
}
internal async Task<Stream> SendAsync(WebSocketRequest request, BucketGroup group, int bucketId, ulong guild Id)
internal async Task<Stream> SendAsync(WebSocketRequest request, BucketGroup group, int bucketId, ulong obj Id)
{
{
request.CancelToken = _cancelToken;
request.CancelToken = _cancelToken;
var bucket = GetBucket(group, bucketId, guild Id);
var bucket = GetBucket(group, bucketId, obj Id);
return await bucket.SendAsync(request).ConfigureAwait(false);
return await bucket.SendAsync(request).ConfigureAwait(false);
}
}
private RequestQueueBucket CreateBucket(BucketDefinition def)
private RequestQueueBucket CreateBucket(Bucket def)
{
{
var parent = def.Parent != null ? GetGlobalBucket(def.Parent.Value) : null;
var parent = def.Parent != null ? GetGlobalBucket(def.Parent.Value) : null;
return new RequestQueueBucket(def.WindowCount, def.WindowSeconds * 1000 , parent);
return new RequestQueueBucket(this, def , parent);
}
}
public void DestroyGuildBucket(GuildBucket type, ulong guildId)
public void DestroyGuildBucket(GuildBucket type, ulong guildId)
@@ -97,15 +128,23 @@ namespace Discord.Net.Queue
RequestQueueBucket bucket;
RequestQueueBucket bucket;
_guildBuckets[(int)type].TryRemove(guildId, out bucket);
_guildBuckets[(int)type].TryRemove(guildId, out bucket);
}
}
public void DestroyChannelBucket(ChannelBucket type, ulong channelId)
{
//Assume this object is locked
RequestQueueBucket bucket;
_channelBuckets[(int)type].TryRemove(channelId, out bucket);
}
private RequestQueueBucket GetBucket(BucketGroup group, int bucketId, ulong guildId)
private RequestQueueBucket GetBucket(BucketGroup group, int bucketId, ulong obj Id)
{
{
switch (group)
switch (group)
{
{
case BucketGroup.Global:
case BucketGroup.Global:
return GetGlobalBucket((GlobalBucket)bucketId);
return GetGlobalBucket((GlobalBucket)bucketId);
case BucketGroup.Guild:
case BucketGroup.Guild:
return GetGuildBucket((GuildBucket)bucketId, guildId);
return GetGuildBucket((GuildBucket)bucketId, objId);
case BucketGroup.Channel:
return GetChannelBucket((ChannelBucket)bucketId, objId);
default:
default:
throw new ArgumentException($"Unknown bucket group: {group}", nameof(group));
throw new ArgumentException($"Unknown bucket group: {group}", nameof(group));
}
}
@@ -118,6 +157,10 @@ namespace Discord.Net.Queue
{
{
return _guildBuckets[(int)type].GetOrAdd(guildId, _ => CreateBucket(_guildLimits[type]));
return _guildBuckets[(int)type].GetOrAdd(guildId, _ => CreateBucket(_guildLimits[type]));
}
}
private RequestQueueBucket GetChannelBucket(ChannelBucket type, ulong channelId)
{
return _channelBuckets[(int)type].GetOrAdd(channelId, _ => CreateBucket(_channelLimits[type]));
}
public async Task ClearAsync()
public async Task ClearAsync()
{
{
@@ -133,5 +176,10 @@ namespace Discord.Net.Queue
}
}
finally { _lock.Release(); }
finally { _lock.Release(); }
}
}
internal async Task RaiseRateLimitTriggered(string id, Bucket bucket, int millis)
{
await RateLimitTriggered.Invoke(id, bucket, millis).ConfigureAwait(false);
}
}
}
}
}