You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

RequestQueueBucket.cs 13 kB

Documentation Overhaul (#1161) * Add XML docs * Clean up style switcher * Squash commits on branch docs/faq-n-patches * Fix broken theme selector * Add local image embed instruction * Add a bunch of XML docs * Add a bunch of XML docs * Fix broken search + DocFX by default ships with an older version of jQuery, switching to a newer version confuses parts of the DocFX Javascript. * Minor fixes for CONTRIBUTING.md and README.md * Clean up filterConfig.yml + New config exposes Discord.Net namespace since it has several common public exceptions that may be helpful to users * Add XML docs * Read token from Environment Variable instead of hardcode * Add XMLDocs * Compress some assets & add OAuth2 URL generator * Fix sample link & add missing pictures * Add tag examples * Fix embed docs consistency * Add details regarding userbot support * Add XML Docs * Add XML Docs * Add XML Docs * Minor fixes in documentations + Fix unescaped '<' + Fix typo * Fix seealso for preconditions and add missing descriptions * Add missing exceptions * Document exposed TypeReaders * Fix letter-casing for files * Add 'last modified' plugin Source: https://github.com/Still34/DocFx.Plugin.LastModified Licensed under MIT License * XML Docs * Fix minor consistencies & redundant impl * Add properties examples to overwrite * Fix missing Username prop * Add warning for bulk-delete endpoint * Replace note block * Add BaseSocketClient docs * Add XML docs * Replace langword null to code block null instead - Because DocFX sucks at rendering langword * Replace all langword placements with code block * Add more IGuild docs * Add details to SpotifyGame * Initial proofread of the articles * Add explanation for RunMode * Add event docs - MessageReceived - ChannelUpdated/Destroyed/Created * Fix light theme link color * Fix xml docs error * Add partial documentation for audit log impl * Add documentation for some REST-based objects * Add partial documentation for audit log objects * Add more XML comments to quotation mark alias map stuff, including an example * Add reference to CommandServiceConfig from the util docs' * Add explanation that if " is removed then it wont work * Fix missing service provider in example * Add documentation for new INestedChannel * Add documentation * Add documentation for new API version & few events * Revise guide paragraphs/samples + Fix various formatting. + Provide a more detailed walkthrough for dependency injection. + Add C# note at intro. * Fix typos & formatting * Improve group module example * Small amount to see if I'm doing it right * Remove/cleanup redundant variables * Fix EnterTypingState impl for doc inheritance * Fix Test to resolve changes made in 15b58e * Improve precondition documentation + Add precondition usage sample + Add precondition group usage sample + Move precondition samples to its own sample folder * Move samples to individual folders * Clarify token source * Cleanup styling of README.md for docs * Replace InvalidPathChars for NS1.3 * InvalidPathChars does not exist in NS1.3; replaced with GetInvalidPathChars instead. * Add a missing change for 2c7cc738 * Update LastModified to v1.1.0 & add license * Rewrite installation page for Core 2.1 * Fix anchor link * Bump post-processor to v1.1.1 * Add fixes to partial file & add license * Moved theme-switcher code to scripts partial file + Add author's MIT license to featherlight javascript * Remove unused bootstrap plugin * Bump LastModified plugin * Changed the path from 'lastmodified' to 'last-modified' for consistency * Cleanup README & Contribution guide * Changes to last pr * Fix GetCategoryAsync docs * Proofread and cleanup articles * Change passive voice in "Get Started" to active * Fix improper preposition in Commands Introduction page * Fix minor grammar mistakes in "Your First Bot" (future tense -> present tense/subjunctive mood -> indicative mood/proper noun casing/incorrect noun/add missing article) * Fix minor grammar mistakes in "Installation" (missing article) * no hablo ingles * Try try try again * I'm sure you're having as much fun as I am * Cleanup TOC & fix titles * Improve styling + Change title font to Noto Sans + Add materialized design for commit message box * Add DescriptionGenerator plugin * Add nightly section for clarification * Fix typos in Nightlies & Post-execution * Bump DescriptionGenerator to v1.1.0 + This build adds the functionality of generating managed references' summary into the description tag. * Initial emoji article draft * Add 'additional information' section for emoji article * Add cosmetic changes to the master css * Alter info box color + Add transition to article content * Add clarification in the emoji article * Emphasize that normal emoji string will not translate to its Unicode representation. * Clean up or add some of the samples featured in the article. + Add emoji/emote declaration section for clarification. + Add WebSocket emote sample. - Remove inconsistent styling ('wacky memes' proves to be too out of place). * Improve readability for nightlies article * Move 'Bundled Preconditions' section * Bump LastModified to fix UTC DateTime parsing * Add langwordMapping.yml * Add XML docs * Add VSC workspace rule * The root workspace limits the ruler to 120 characters for member documentations and excludes folders such as 'samples' and 'docs'. * The docs workspace limits the ruler to 70 characters for standard conceptual article to comply with documentation's CONTRIBUTING.md rule, and excludes temprorary folders created by DocFX. * Update CONTRIBUTING.md * Add documentation style rule * Fix styling of several member documentation * Fix ' />' caused by Agent Smith oddities * Fix styling to be more specific about the mention of IDs * Fix exception summary to comply with official Microsoft Docs style * References https://docs.microsoft.com/en-us/dotnet/api/system.argumentnullexception?view=netframework-4.7.2 https://docs.microsoft.com/en-us/dotnet/api/system.platformnotsupportedexception?view=netframework-4.7.2 https://docs.microsoft.com/en-us/dotnet/api/system.badimageformatexception?view=netframework-4.7.2 * Add XML documentations * Shift color return docs * Fix minor docs * Added documentation for SocketDMChannel, SocketGuildChannel, and SocketTextChannel * Add XML docs * Corrections to SocketGuildChannel * Corrections to SocketTextChannel * Corrections to SocketDMChannel * Swapped out 'id' for 'snowflake identifier * Swapped out 'id' for 'snowflake identifier' * SocketDMChannel amendments * SocketGuildChannel amendments * SocketTextChannel amendments * Add XML docs & patch return types + Starting from this commit, all return types for tasks will use style similar to most documentations featured on docs.microsoft.com References: https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.-ctor?view=efcore-2.1 https://docs.microsoft.com/en-us/dotnet/api/system.io.filestream.readasync?view=netcore-2.1 https://docs.microsoft.com/en-us/dotnet/api/system.io.textwriter.writelineasync?view=netcore-2.1#System_IO_TextWriter_WriteLineAsync_System_Char___ And many more other asynchronous method documentations featured in the latest BCL. * Added documentation for many audit log data types, fixed vowel indefinite articles * Change audit log data types to start with 'Contains' (verb) instead of an article * Fix some documentation issues and document some more audit log data types * Fix English posession * Add XML doc * Documented two more types * Documented RoleCreateAuditLogData * Document remaining audit log data types * Added RestDMChannel documentation * Added RestGuildChannel documentation * Added RestTextChannel documentation * Added RestVoiceChannel documentation * Added RestUser documentation * Added RestRole documentation * Added RestMessage documentation * Slightly better wording * Contains -> Contains a piece of (describe article) * [EN] Present perf. -> past perf. * Add XML docs * Fix arrow alignment * Clarify supported nullable type * Fixed a typo in ISnowflakeEntity * Added RestUser Documentation * Added RestInvite documentation * Add XML docs & minor optimizations * Minor optimization for doc rendering * Rollback font optimization changes * Amendments to RestUser * Added SocketDMChannel documentation * Added RestDMChannel documentation * Added RestGuild documentation * Adjustment to SocketDMChannel * Added minimal descriptions from the API documentation for Integration types * Added obsolete mention to the ReadMessages flag. * Added remarks about 2FA requirement for guild permissions * Added xmldoc for GuildPermission methods * Added xml doc for ToAllowList and ToDenyList * Added specification of how the bits of the color raw value are packed * Added discord API documentation to IConnection interface * I can spell :^) * Fix whitespace in ChannelPermission * fix spacing of values in guildpermission * Made changes to get field descriptions from feedback, added returns tag to IConnection * Added property get standard for IntegrationAccount * Added property get pattern to xml docs and identical returns tag. * Change all color class references to struct ...because it isn't a class. * Add XML docs * Rewrote the returns tags in IGuildIntegration, removed the ones I was unsure about. * Rewrote the rest of the returns tags * Amendments * Cleanup doc for c1d78189 * Added types to <returns> tags where missing * Added second sample for adding reactions * Added some class summaries * Missed a period * Amendments * restored the removed line break * Removed unnecessary see tag * Use consistent quotation marks around subscribers, the name for these users are dependant on the source of where they are integrated from (youtube or twitch), so we should not use a name that is specific to one platform * Add <remarks> tag to the IGuildIntegration xmldocs * Fix grammar issue * Update DescriptionGenerator * Cleanup of https://github.com/Still34/Discord.Net/pull/8 * Cleanup previous PR * Fix for misleading behaviour in the emoji guide + Original lines stated that sending a emoji wrapped in colon will not be parsed, but that was incorrect; replaced with reactions instead of sending messages as the example * Add strings for dictionary in DotSettings * Add XML docs * Fix lots of typos in comments + Geez, I didn't know there were so many. * Add XML docs & rewrite GetMessagesAsync docs This commit rewrites the remarks section of GetMessagesAsync, as well as adding examples to several methods. * Update 'Your First Bot' + This commit reflects the new changes made to the Discord Application Developer Portal after its major update * Initial optimization for DocFX render & add missing files * Add examples in message methods * Cleanup https://github.com/RogueException/Discord.Net/pull/1128 * Fix first bot note * Cleanup FAQ structure * Add XML docs * Update docfx plugins * Fix navbar collapsing issue * Fix broken xref * Cleanup FAQ section + Add introductory paragraphs to each FAQ section. + Add 'missing dependency' entry to commands FAQ. * Split commands FAQ to 'General' and 'DI' sections. * Cleanup https://github.com/RogueException/Discord.Net/pull/1139 * Fix missing namespace * Add missing highlighting css for the light theme * Add additional clarification for installing packages * Add indentation to example for clarity * Cleanup several articles to be more human-friendly and easier to read * Remove RPC-related notes * Cleanup slow-mode-related documentation strings * Add an additional note about cross-guild emote usage * Add CreateTextChannel sample * Add XMLDocs
6 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. using Newtonsoft.Json;
  2. using Newtonsoft.Json.Linq;
  3. using System;
  4. #if DEBUG_LIMITS
  5. using System.Diagnostics;
  6. #endif
  7. using System.IO;
  8. using System.Net;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. namespace Discord.Net.Queue
  12. {
  13. internal class RequestBucket
  14. {
  15. private readonly object _lock;
  16. private readonly RequestQueue _queue;
  17. private int _semaphore;
  18. private DateTimeOffset? _resetTick;
  19. public string Id { get; private set; }
  20. public int WindowCount { get; private set; }
  21. public DateTimeOffset LastAttemptAt { get; private set; }
  22. public RequestBucket(RequestQueue queue, RestRequest request, string id)
  23. {
  24. _queue = queue;
  25. Id = id;
  26. _lock = new object();
  27. if (request.Options.IsClientBucket)
  28. WindowCount = ClientBucket.Get(request.Options.BucketId).WindowCount;
  29. else
  30. WindowCount = 1; //Only allow one request until we get a header back
  31. _semaphore = WindowCount;
  32. _resetTick = null;
  33. LastAttemptAt = DateTimeOffset.UtcNow;
  34. }
  35. static int nextId = 0;
  36. public async Task<Stream> SendAsync(RestRequest request)
  37. {
  38. int id = Interlocked.Increment(ref nextId);
  39. #if DEBUG_LIMITS
  40. Debug.WriteLine($"[{id}] Start");
  41. #endif
  42. LastAttemptAt = DateTimeOffset.UtcNow;
  43. while (true)
  44. {
  45. await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false);
  46. await EnterAsync(id, request).ConfigureAwait(false);
  47. #if DEBUG_LIMITS
  48. Debug.WriteLine($"[{id}] Sending...");
  49. #endif
  50. RateLimitInfo info = default(RateLimitInfo);
  51. try
  52. {
  53. var response = await request.SendAsync().ConfigureAwait(false);
  54. info = new RateLimitInfo(response.Headers);
  55. if (response.StatusCode < (HttpStatusCode)200 || response.StatusCode >= (HttpStatusCode)300)
  56. {
  57. switch (response.StatusCode)
  58. {
  59. case (HttpStatusCode)429:
  60. if (info.IsGlobal)
  61. {
  62. #if DEBUG_LIMITS
  63. Debug.WriteLine($"[{id}] (!) 429 [Global]");
  64. #endif
  65. _queue.PauseGlobal(info);
  66. }
  67. else
  68. {
  69. #if DEBUG_LIMITS
  70. Debug.WriteLine($"[{id}] (!) 429");
  71. #endif
  72. UpdateRateLimit(id, request, info, true);
  73. }
  74. await _queue.RaiseRateLimitTriggered(Id, info).ConfigureAwait(false);
  75. continue; //Retry
  76. case HttpStatusCode.BadGateway: //502
  77. #if DEBUG_LIMITS
  78. Debug.WriteLine($"[{id}] (!) 502");
  79. #endif
  80. if ((request.Options.RetryMode & RetryMode.Retry502) == 0)
  81. throw new HttpException(HttpStatusCode.BadGateway, request, null);
  82. continue; //Retry
  83. default:
  84. int? code = null;
  85. string reason = null;
  86. if (response.Stream != null)
  87. {
  88. try
  89. {
  90. using (var reader = new StreamReader(response.Stream))
  91. using (var jsonReader = new JsonTextReader(reader))
  92. {
  93. var json = JToken.Load(jsonReader);
  94. try { code = json.Value<int>("code"); } catch { };
  95. try { reason = json.Value<string>("message"); } catch { };
  96. }
  97. }
  98. catch { }
  99. }
  100. throw new HttpException(response.StatusCode, request, code, reason);
  101. }
  102. }
  103. else
  104. {
  105. #if DEBUG_LIMITS
  106. Debug.WriteLine($"[{id}] Success");
  107. #endif
  108. return response.Stream;
  109. }
  110. }
  111. //catch (HttpException) { throw; } //Pass through
  112. catch (TimeoutException)
  113. {
  114. #if DEBUG_LIMITS
  115. Debug.WriteLine($"[{id}] Timeout");
  116. #endif
  117. if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0)
  118. throw;
  119. await Task.Delay(500).ConfigureAwait(false);
  120. continue; //Retry
  121. }
  122. /*catch (Exception)
  123. {
  124. #if DEBUG_LIMITS
  125. Debug.WriteLine($"[{id}] Error");
  126. #endif
  127. if ((request.Options.RetryMode & RetryMode.RetryErrors) == 0)
  128. throw;
  129. await Task.Delay(500);
  130. continue; //Retry
  131. }*/
  132. finally
  133. {
  134. UpdateRateLimit(id, request, info, false);
  135. #if DEBUG_LIMITS
  136. Debug.WriteLine($"[{id}] Stop");
  137. #endif
  138. }
  139. }
  140. }
  141. private async Task EnterAsync(int id, RestRequest request)
  142. {
  143. int windowCount;
  144. DateTimeOffset? resetAt;
  145. bool isRateLimited = false;
  146. while (true)
  147. {
  148. if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested)
  149. {
  150. if (!isRateLimited)
  151. throw new TimeoutException();
  152. else
  153. ThrowRetryLimit(request);
  154. }
  155. lock (_lock)
  156. {
  157. windowCount = WindowCount;
  158. resetAt = _resetTick;
  159. }
  160. DateTimeOffset? timeoutAt = request.TimeoutAt;
  161. if (windowCount > 0 && Interlocked.Decrement(ref _semaphore) < 0)
  162. {
  163. if (!isRateLimited)
  164. {
  165. isRateLimited = true;
  166. await _queue.RaiseRateLimitTriggered(Id, null).ConfigureAwait(false);
  167. }
  168. ThrowRetryLimit(request);
  169. if (resetAt.HasValue)
  170. {
  171. if (resetAt > timeoutAt)
  172. ThrowRetryLimit(request);
  173. int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds);
  174. #if DEBUG_LIMITS
  175. Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)");
  176. #endif
  177. if (millis > 0)
  178. await Task.Delay(millis, request.Options.CancelToken).ConfigureAwait(false);
  179. }
  180. else
  181. {
  182. if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0)
  183. ThrowRetryLimit(request);
  184. #if DEBUG_LIMITS
  185. Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)");
  186. #endif
  187. await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false);
  188. }
  189. continue;
  190. }
  191. #if DEBUG_LIMITS
  192. else
  193. Debug.WriteLine($"[{id}] Entered Semaphore ({_semaphore}/{WindowCount} remaining)");
  194. #endif
  195. break;
  196. }
  197. }
  198. private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429)
  199. {
  200. if (WindowCount == 0)
  201. return;
  202. lock (_lock)
  203. {
  204. bool hasQueuedReset = _resetTick != null;
  205. if (info.Limit.HasValue && WindowCount != info.Limit.Value)
  206. {
  207. WindowCount = info.Limit.Value;
  208. _semaphore = info.Remaining.Value;
  209. #if DEBUG_LIMITS
  210. Debug.WriteLine($"[{id}] Upgraded Semaphore to {info.Remaining.Value}/{WindowCount}");
  211. #endif
  212. }
  213. var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
  214. DateTimeOffset? resetTick = null;
  215. //Using X-RateLimit-Remaining causes a race condition
  216. /*if (info.Remaining.HasValue)
  217. {
  218. Debug.WriteLine($"[{id}] X-RateLimit-Remaining: " + info.Remaining.Value);
  219. _semaphore = info.Remaining.Value;
  220. }*/
  221. if (info.RetryAfter.HasValue)
  222. {
  223. //RetryAfter is more accurate than Reset, where available
  224. resetTick = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value);
  225. #if DEBUG_LIMITS
  226. Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)");
  227. #endif
  228. }
  229. else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false))
  230. {
  231. resetTick = DateTimeOffset.Now.Add(info.ResetAfter.Value);
  232. }
  233. else if (info.Reset.HasValue)
  234. {
  235. resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0);
  236. /* millisecond precision makes this unnecessary, retaining in case of regression
  237. if (request.Options.IsReactionBucket)
  238. resetTick = DateTimeOffset.Now.AddMilliseconds(250);
  239. */
  240. int diff = (int)(resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds;
  241. #if DEBUG_LIMITS
  242. Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)");
  243. #endif
  244. }
  245. else if (request.Options.IsClientBucket && request.Options.BucketId != null)
  246. {
  247. resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(request.Options.BucketId).WindowSeconds);
  248. #if DEBUG_LIMITS
  249. Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)");
  250. #endif
  251. }
  252. if (resetTick == null)
  253. {
  254. WindowCount = 0; //No rate limit info, disable limits on this bucket (should only ever happen with a user token)
  255. #if DEBUG_LIMITS
  256. Debug.WriteLine($"[{id}] Disabled Semaphore");
  257. #endif
  258. return;
  259. }
  260. if (!hasQueuedReset || resetTick > _resetTick)
  261. {
  262. _resetTick = resetTick;
  263. LastAttemptAt = resetTick.Value; //Make sure we dont destroy this until after its been reset
  264. #if DEBUG_LIMITS
  265. Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms");
  266. #endif
  267. if (!hasQueuedReset)
  268. {
  269. var _ = QueueReset(id, (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds));
  270. }
  271. }
  272. }
  273. }
  274. private async Task QueueReset(int id, int millis)
  275. {
  276. while (true)
  277. {
  278. if (millis > 0)
  279. await Task.Delay(millis).ConfigureAwait(false);
  280. lock (_lock)
  281. {
  282. millis = (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds);
  283. if (millis <= 0) //Make sure we havent gotten a more accurate reset time
  284. {
  285. #if DEBUG_LIMITS
  286. Debug.WriteLine($"[{id}] * Reset *");
  287. #endif
  288. _semaphore = WindowCount;
  289. _resetTick = null;
  290. return;
  291. }
  292. }
  293. }
  294. }
  295. private void ThrowRetryLimit(RestRequest request)
  296. {
  297. if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0)
  298. throw new RateLimitedException(request);
  299. }
  300. }
  301. }