C# sliding window throttle implementation
This is the throttle implementation used by Soapi.CS. It is compatible with all .net platforms (desktop/silverlight/phone/mono)
It has proven to provide maximum practical API throughput while complying both with the letter of the law as well as compensating for incidental vagaries. I would go so far as to characterize it as bullet-proof.
As a demonstration, I pulled all of Jon Skeet's, questions/answers/rep changes/timelines, on all Stack Exchange sites, in less than 3 minutes (560 requests), using the simple code listed after the throttle. Here is the log.
Soapi.Net.RequestThrottle
//
// Project: SOAPI
// http://soapics.codeplex.com
// https://stackapps.com/questions/386
//
// Copyright 2010, Sky Sanders
// Licensed under the GPL Version 2 license.
// http://soapi.codeplex.com/license
//
#region
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Threading;
#endregion
namespace Soapi.Net
{
/// <summary>
/// This is a fully configurable, thread-safe web request throttle that fully complies with the
/// published usage guidelines. In addition to compliance with the letter of the law, testing
/// has exposed further limitations that are compensated. See code comments for more detail.
///
/// Simply route all WebRequest.Create calls through RequestThrottle.Instance.Create();
///
/// Upon completion of an issued request, regardless of status, you must call
/// RequestThrottle.Instance.Complete() to decrement the outstanding request count.
///
/// NOTE: you can use this as a global throttle using WebRequest.RegisterPrefix
/// http://msdn.microsoft.com/en-us/library/system.net.webrequest.registerprefix.aspx
/// but this is not a viable option for silverlight so in Soapi, where requests
/// are created in one place, we just call it explicitly.
/// </summary>
/// <remarks>
/// Throttling conversation here: https://stackapps.com/questions/1143/request-throttling-limits
/// </remarks>
public sealed class RequestThrottle // : IWebRequestCreate
{
#region Fields
private int _outstandingRequests;
private readonly Queue<DateTime> _requestTimes = new Queue<DateTime>();
#endregion
#region Constructors
private RequestThrottle()
{
ThrottleWindowTime = new TimeSpan(0, 0, 0, 5);
ThrottleWindowCount = 30;
MaxPendingRequests = 15;
}
#endregion
#region Properties
public static RequestThrottle Instance
{
get { return Nested.instance; }
}
/// <summary>
/// The maximum number of allowed pending request.
///
/// The throttle window will keep us in compliance with the
/// letter of the law, but testing has shown that a large
/// number of outstanding requests result in a cascade of
/// (500) errors that does not stop.
///
/// So we will block while there are > MaxPendingRequests
/// regardless of throttle window.
///
/// Defaults to 15 which has proven to be reliable.
/// </summary>
public int MaxPendingRequests { get; set; }
/// <summary>
/// If you are interested in monitoring
/// </summary>
public int OutstandingRequests
{
get { return _outstandingRequests; }
}
/// <summary>
/// The quantitive portion (xxx) of the of 30 requests per 5 seconds
/// Defaults to published guidelines of 5 seconds
/// </summary>
public int ThrottleWindowCount { get; set; }
/// <summary>
/// The temporal portion (yyy) of the of 30 requests per 5 seconds
/// Defaults to the published guidelines of 30
/// </summary>
public TimeSpan ThrottleWindowTime { get; set; }
#endregion
#region Public Methods
/// <summary>
/// This decrements the outstanding request count.
///
/// This MUST MUST MUST be called when a request has
/// completed regardless of status.
///
/// If a request fails, it may be wise to delay calling
/// this, e.g. cool down, for a few seconds, before
/// reissuing the request.
/// </summary>
public void Complete()
{
_outstandingRequests--;
}
/// <summary>
/// Create a WebRequest. This method will block if too many
/// outstanding requests are pending or the throttle window
/// threshold has been reached.
/// </summary>
/// <param name = "uri"></param>
/// <returns></returns>
public WebRequest Create(Uri uri)
{
lock (typeof(ThrottleLock))
{
// note: we could use a list of WeakReferences and
// may do so at a later date, but for now, this
// works just fine as long as you call .Complete
_outstandingRequests++;
while (_outstandingRequests > MaxPendingRequests)
{
using (var throttleGate = new AutoResetEvent(false))
{
throttleGate.WaitOne(100);
}
}
if (_requestTimes.Count == ThrottleWindowCount)
{
// pull the earliest request of the bottom
DateTime tail = _requestTimes.Dequeue();
// calculate the interval between now (head) and tail
// to determine if we need to chill out for a few millisecons
TimeSpan waitTime = (ThrottleWindowTime - (DateTime.Now - tail));
if (waitTime.TotalMilliseconds > 0)
{
#if !SILVERLIGHT
Trace.WriteLine("waiting:\t" + waitTime + "\t" + uri.AbsoluteUri);
#endif
using (var throttleGate = new AutoResetEvent(false))
{
throttleGate.WaitOne(waitTime);
}
}
}
// good to go.
_requestTimes.Enqueue(DateTime.Now);
return WebRequest.Create(uri);
}
}
/// <summary>
/// Create a WebRequest. This method will block if too many
/// outstanding requests are pending or the throttle window
/// threshold has been reached.
/// </summary>
/// <param name = "url"></param>
/// <returns></returns>
public WebRequest Create(string url)
{
return Create(new Uri(url));
}
#endregion
/// <summary>
/// lock handle
/// </summary>
private class ThrottleLock
{
}
#region Singleton Plubming
// the skeet singleton implementation
// http://www.yoda.arachsys.com/csharp/singleton.html
// ReSharper disable ClassNeverInstantiated.Local
class Nested
// ReSharper restore ClassNeverInstantiated.Local
{
// ReSharper disable EmptyConstructor
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
// ReSharper restore EmptyConstructor
{
}
internal static readonly RequestThrottle instance = new RequestThrottle();
}
#endregion
}
}
Demo
Simple app, using Soapi.CS that will gather all data for a user on all SE API sites. Simply supply an endpoint and user id to use as reference.
Useful as a throttle test.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Soapi;
using Soapi.Domain;
namespace Samples
{
public class AllMyStuff
{
private const string ApiKey = "your-key-here";
private readonly ApiContext Context;
private readonly List<UserData> Users;
public AllMyStuff(string endpoint, int userId)
{
Context = new ApiContext(ApiKey);
Context.Options.LazyLoadingEnabled(true);
Context.Initialize(false);
Users = Context.Sites.First(s => s.ApiEndpoint.StartsWith(endpoint))
.User(userId).Associated.Select(u => new UserData(u)).ToList();
var initEvent = new AutoResetEvent(false);
foreach (var item in Users)
{
UserData userData = item;
ThreadPool.QueueUserWorkItem(o =>
{
User account = userData.User;
userData.Questions = account.Questions
.PageCount(-1)
.FromDate(new DateTime(1980, 1, 1))
.ToDate(new DateTime(2020, 1, 1))
.ToList();
userData.Answers = account.Answers
.PageCount(-1)
.FromDate(new DateTime(1980, 1, 1))
.ToDate(new DateTime(2020, 1, 1))
.ToList();
userData.Reputation = account
.RepChanges
.PageCount(-1)
.FromDate(new DateTime(1980, 1, 1))
.ToDate(new DateTime(2020, 1, 1))
.ToList();
userData.Timeline = account
.Timeline
.PageCount(-1)
.FromDate(new DateTime(1980, 1, 1))
.ToDate(new DateTime(2020,1, 1))
.ToList();
userData.Initialized = true;
lock (Users)
if (Users.All(u => u.Initialized))
initEvent.Set();
});
}
initEvent.WaitOne();
}
}
public class UserData
{
public UserData(User user)
{
User = user;
ApiEndpoint = user.Site.ApiEndpoint;
}
public List<Answer> Answers { get; set; }
public string ApiEndpoint { get; set; }
public bool Initialized { get; set; }
public List<Question> Questions { get; set; }
public List<RepChange> Reputation { get; set; }
public List<UserTimeline> Timeline { get; set; }
public User User { get; set; }
}
}