Permalink
Browse files

Add TrickleRateLimiter ThrottlingIntegrationProxy (and tests)

  • Loading branch information...
1 parent a01eac2 commit a38c766774f4fb2e222c9b4f4d0d4e26c94489ef @danbarratt danbarratt committed Nov 17, 2011
View
BIN libraries/Rhino.Mocks.dll
Binary file not shown.
View
65 source/XeroApi.Tests/ThrottlingIntegrationProxyTests.cs
@@ -0,0 +1,65 @@
+using System;
+using NUnit.Framework;
+using Rhino.Mocks;
+using XeroApi.Integration;
+
+namespace XeroApi.Tests
+{
+ public class ThrottlingIntegrationProxyTests
+ {
+ [Test]
+ public void it_enforces_the_rate_limiter_and_delegates_to_its_inner_integration_proxy_for_each_invocation()
+ {
+ var inner = MockRepository.GenerateStub<IIntegrationProxy>();
+
+ var rateLimiter = MockRepository.GenerateStub<IRateLimiter>();
+ var integrationProxy = new ThrottlingIntegrationProxy(inner, rateLimiter);
+
+ integrationProxy.CreateAttachment(null, null, null);
+ rateLimiter.AssertWasCalled(it => it.CheckAndEnforceRateLimit(Arg<DateTime>.Is.Anything), opt => opt.Repeat.Once());
+ inner.AssertWasCalled(it => it.CreateAttachment(null, null, null));
+
+ rateLimiter = MockRepository.GenerateStub<IRateLimiter>();
+ integrationProxy = new ThrottlingIntegrationProxy(inner, rateLimiter);
+ integrationProxy.CreateElements(null, null);
+ rateLimiter.AssertWasCalled(it => it.CheckAndEnforceRateLimit(Arg<DateTime>.Is.Anything), opt => opt.Repeat.Once());
+ inner.AssertWasCalled(it => it.CreateElements(null, null));
+
+ rateLimiter = MockRepository.GenerateStub<IRateLimiter>();
+ integrationProxy = new ThrottlingIntegrationProxy(inner, rateLimiter);
+ integrationProxy.FindAttachments(null, null);
+ rateLimiter.AssertWasCalled(it => it.CheckAndEnforceRateLimit(Arg<DateTime>.Is.Anything), opt => opt.Repeat.Once());
+ inner.AssertWasCalled(it => it.FindAttachments(null, null));
+
+ rateLimiter = MockRepository.GenerateStub<IRateLimiter>();
+ integrationProxy = new ThrottlingIntegrationProxy(inner, rateLimiter);
+ integrationProxy.FindOneAttachment(null, null, null);
+ rateLimiter.AssertWasCalled(it => it.CheckAndEnforceRateLimit(Arg<DateTime>.Is.Anything), opt => opt.Repeat.Once());
+ inner.AssertWasCalled(it => it.FindOneAttachment(null, null, null));
+
+ rateLimiter = MockRepository.GenerateStub<IRateLimiter>();
+ integrationProxy = new ThrottlingIntegrationProxy(inner, rateLimiter);
+ integrationProxy.UpdateOrCreateAttachment(null, null, null);
+ rateLimiter.AssertWasCalled(it => it.CheckAndEnforceRateLimit(Arg<DateTime>.Is.Anything), opt => opt.Repeat.Once());
+ inner.AssertWasCalled(it => it.UpdateOrCreateAttachment(null, null, null));
+
+ rateLimiter = MockRepository.GenerateStub<IRateLimiter>();
+ integrationProxy = new ThrottlingIntegrationProxy(inner, rateLimiter);
+ integrationProxy.UpdateOrCreateElements(null, null);
+ rateLimiter.AssertWasCalled(it => it.CheckAndEnforceRateLimit(Arg<DateTime>.Is.Anything), opt => opt.Repeat.Once());
+ inner.AssertWasCalled(it => it.UpdateOrCreateElements(null, null));
+
+ rateLimiter = MockRepository.GenerateStub<IRateLimiter>();
+ integrationProxy = new ThrottlingIntegrationProxy(inner, rateLimiter);
+ integrationProxy.FindOne(null, null, null);
+ rateLimiter.AssertWasCalled(it => it.CheckAndEnforceRateLimit(Arg<DateTime>.Is.Anything), opt => opt.Repeat.Once());
+ inner.AssertWasCalled(it => it.FindOne(null, null, null));
+
+ rateLimiter = MockRepository.GenerateStub<IRateLimiter>();
+ integrationProxy = new ThrottlingIntegrationProxy(inner, rateLimiter);
+ integrationProxy.FindElements(null);
+ rateLimiter.AssertWasCalled(it => it.CheckAndEnforceRateLimit(Arg<DateTime>.Is.Anything), opt => opt.Repeat.Once());
+ inner.AssertWasCalled(it => it.FindElements(null));
+ }
+ }
+}
View
176 source/XeroApi.Tests/TrickleRateLimiterTests.cs
@@ -0,0 +1,176 @@
+using System;
+using NUnit.Framework;
+using Rhino.Mocks;
+using XeroApi.Integration;
+
+namespace XeroApi.Tests
+{
+ public class TrickleRateLimiterTests
+ {
+
+ [Test]
+ public void first_event_doesnt_require_rate_limiting()
+ {
+ TrickleRateLimiter rateLimiter = new TrickleRateLimiter(MockRepository.GenerateStub<IEventTimeline>());
+ Assert.DoesNotThrow(() => rateLimiter.CheckAndEnforceRateLimit(DateTime.UtcNow));
+ }
+
+ [Test]
+ public void first_event_is_recorded_in_timeline()
+ {
+ var timeline = MockRepository.GenerateStub<IEventTimeline>();
+ TrickleRateLimiter rateLimiter = new TrickleRateLimiter(timeline);
+
+ Assert.DoesNotThrow(() => rateLimiter.CheckAndEnforceRateLimit(DateTime.UtcNow));
+
+ timeline.AssertWasCalled(it => it.RecordEvent(Arg<DateTime>.Is.Anything));
+ }
+
+ [Test]
+ public void it_asks_the_timeline_for_the_date_and_time_of_the_last_event()
+ {
+ var timeline = MockRepository.GenerateStub<IEventTimeline>();
+ timeline.Stub(it => it.RecordEvent(Arg<DateTime>.Is.Anything));
+
+ TrickleRateLimiter rateLimiter = new TrickleRateLimiter(timeline);
+
+ rateLimiter.CheckAndEnforceRateLimit(DateTime.UtcNow);
+
+ timeline.AssertWasCalled(it => it.GetLastEventDateAndTime());
+ }
+
+ [Test]
+ public void it_records_the_event_AFTER_asking_the_timeline_for_the_date_and_time_of_the_last_event()
+ {
+ var mocks = new MockRepository();
+
+ var timeline = mocks.StrictMock<IEventTimeline>();
+
+ using (mocks.Ordered())
+ {
+ Expect.Call(timeline.GetLastEventDateAndTime()).Return(DateTime.Parse("19-Nov-1976"));
+ Expect.Call(() => timeline.RecordEvent(Arg<DateTime>.Is.Anything));
+ }
+
+ mocks.ReplayAll();
+
+ TrickleRateLimiter rateLimiter = new TrickleRateLimiter(timeline);
+ rateLimiter.CheckAndEnforceRateLimit(DateTime.UtcNow);
+
+ mocks.VerifyAll();
+ }
+
+ [Test]
+ public void it_doesnt_trigger_the_rate_limit_when_the_timeline_is_empty()
+ {
+ var timeline = MockRepository.GenerateStub<IEventTimeline>();
+ timeline.Stub(it => it.RecordEvent(Arg<DateTime>.Is.Anything));
+ timeline.Stub(it => it.GetLastEventDateAndTime()).Return(null);
+
+ IXXX xxx = MockRepository.GenerateMock<IXXX>();
+
+ TrickleRateLimiter rateLimiter = new TrickleRateLimiter(timeline, xxx);
+ rateLimiter.CheckAndEnforceRateLimit(DateTime.UtcNow);
+
+ xxx.AssertWasNotCalled(it => it.PauseBeforeEvent(Arg<TimeSpan>.Is.Anything));
+ }
+
+ [Test]
+ public void it_doesnt_trigger_the_rate_limit_when_the_last_event_date_is_more_than_1_second_ago()
+ {
+ DateTime firstEventDateTime = new DateTime(2000, 1, 1, 12, 0, 0);
+ DateTime secondEventDateTime = new DateTime(2000, 1, 1, 12, 0, 2);
+
+ var timeline = MockRepository.GenerateStub<IEventTimeline>();
+ timeline.Stub(it => it.RecordEvent(Arg<DateTime>.Is.Anything));
+ timeline.Stub(it => it.GetLastEventDateAndTime()).Return(firstEventDateTime);
+
+ IXXX xxx = MockRepository.GenerateMock<IXXX>();
+
+ TrickleRateLimiter rateLimiter = new TrickleRateLimiter(timeline, xxx);
+ rateLimiter.CheckAndEnforceRateLimit(secondEventDateTime);
+
+ xxx.AssertWasNotCalled(it => it.PauseBeforeEvent(Arg<TimeSpan>.Is.Anything));
+ }
+
+ [Test]
+ public void it_doesnt_trigger_the_rate_limit_when_the_last_event_date_is_exactly_1_second_ago()
+ {
+ DateTime firstEventDateTime = new DateTime(2000, 1, 1, 12, 0, 0);
+ DateTime secondEventDateTime = firstEventDateTime.AddSeconds(1);
+
+ var timeline = MockRepository.GenerateStub<IEventTimeline>();
+ timeline.Stub(it => it.RecordEvent(Arg<DateTime>.Is.Anything));
+ timeline.Stub(it => it.GetLastEventDateAndTime()).Return(firstEventDateTime);
+
+ IXXX xxx = MockRepository.GenerateMock<IXXX>();
+
+ TrickleRateLimiter rateLimiter = new TrickleRateLimiter(timeline, xxx);
+ rateLimiter.CheckAndEnforceRateLimit(secondEventDateTime);
+
+ xxx.AssertWasNotCalled(it => it.PauseBeforeEvent(Arg<TimeSpan>.Is.Anything));
+ }
+
+ [Test]
+ public void it_does_trigger_the_rate_limit_when_the_last_event_date_is_less_than_1_second_ago()
+ {
+ DateTime firstEventDateTime = new DateTime(2000, 1, 1, 12, 0, 0);
+ DateTime secondEventDateTime = new DateTime(2000, 1, 1, 12, 0, 0, 500);
+
+ var timeline = MockRepository.GenerateStub<IEventTimeline>();
+ timeline.Stub(it => it.RecordEvent(Arg<DateTime>.Is.Anything));
+ timeline.Stub(it => it.GetLastEventDateAndTime()).Return(firstEventDateTime);
+
+ IXXX xxx = MockRepository.GenerateMock<IXXX>();
+
+ TrickleRateLimiter rateLimiter = new TrickleRateLimiter(timeline, xxx);
+ rateLimiter.CheckAndEnforceRateLimit(secondEventDateTime);
+
+ xxx.AssertWasCalled(it => it.PauseBeforeEvent(Arg<TimeSpan>.Is.Anything));
+ }
+
+ [Test]
+ public void it_does_trigger_the_rate_limit_for_enough_time_to_maintain_one_event_per_second()
+ {
+ DateTime firstEventDateTime = new DateTime(2000, 1, 1, 12, 0, 0);
+ DateTime secondEventDateTime = firstEventDateTime.AddMilliseconds(400); // <-- Second event is 400ms after the first event. This should trigger a pause of 600ms.
+
+ var timeline = MockRepository.GenerateStub<IEventTimeline>();
+ timeline.Stub(it => it.RecordEvent(Arg<DateTime>.Is.Anything));
+ timeline.Stub(it => it.GetLastEventDateAndTime()).Return(firstEventDateTime);
+
+ IXXX xxx = MockRepository.GenerateMock<IXXX>();
+
+ TrickleRateLimiter rateLimiter = new TrickleRateLimiter(timeline, xxx);
+ rateLimiter.CheckAndEnforceRateLimit(secondEventDateTime);
+
+ xxx.AssertWasCalled(it => it.PauseBeforeEvent(Arg<TimeSpan>.Is.Equal(TimeSpan.FromMilliseconds(600))));
+ }
+
+ [Test]
+ public void it_records_the_event_AFTER_pausing_for_the_current_event()
+ {
+ DateTime firstEventDateTime = new DateTime(2000, 1, 1, 12, 0, 0);
+ DateTime secondEventDateTime = firstEventDateTime.AddMilliseconds(400); // <-- Second event is 400ms after the first event. This should trigger a pause of 600ms.
+
+ var mocks = new MockRepository();
+
+ var timeline = mocks.StrictMock<IEventTimeline>();
+ var xxx = mocks.StrictMock<IXXX>();
+
+ using (mocks.Ordered())
+ {
+ Expect.Call(timeline.GetLastEventDateAndTime()).Return(firstEventDateTime);
+ Expect.Call(() => xxx.PauseBeforeEvent(Arg<TimeSpan>.Is.Anything));
+ Expect.Call(() => timeline.RecordEvent(Arg<DateTime>.Is.Anything));
+ }
+
+ mocks.ReplayAll();
+
+ TrickleRateLimiter rateLimiter = new TrickleRateLimiter(timeline, xxx);
+ rateLimiter.CheckAndEnforceRateLimit(secondEventDateTime);
+
+ mocks.VerifyAll();
+ }
+ }
+}
View
5 source/XeroApi.Tests/XeroApi.Tests.csproj
@@ -34,6 +34,9 @@
<Reference Include="nunit.framework">
<HintPath>..\..\libraries\nunit.framework.dll</HintPath>
</Reference>
+ <Reference Include="Rhino.Mocks">
+ <HintPath>..\..\libraries\Rhino.Mocks.dll</HintPath>
+ </Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
@@ -45,8 +48,10 @@
<Compile Include="IntegrationProxyHelperTests.cs" />
<Compile Include="ModelSerializationTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="SimpleRateLimiterTests.cs" />
<Compile Include="Stubs\StubContact.cs" />
<Compile Include="Stubs\StubIntegrationProxy.cs" />
+ <Compile Include="ThrottlingIntegrationProxyTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\XeroApi\XeroApi.csproj">
View
15 source/XeroApi/Integration/IRateLimiter.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Net;
+
+namespace XeroApi.Integration
+{
+ public interface IRateLimiter
+ {
+ /// <summary>
+ /// Checks and enforces the current rate limit.
+ /// </summary>
+ /// <param name="eventTimestamp">The event timestamp.</param>
+ void CheckAndEnforceRateLimit(DateTime eventTimestamp);
+
+ }
+}
View
18 source/XeroApi/Integration/NullRateLimiter.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Net;
+
+namespace XeroApi.Integration
+{
+ /// <summary>
+ /// Null implementation of <c ref="IRateLimiter" />.
+ /// </summary>
+ public class NullRateLimiter : IRateLimiter
+ {
+ /// <summary>
+ /// Checks and enforces the current rate limit.
+ /// </summary>
+ public void CheckAndEnforceRateLimit(DateTime eventDate)
+ {
+ }
+ }
+}
View
75 source/XeroApi/Integration/ThrottlingIntegrationProxy.cs
@@ -0,0 +1,75 @@
+using System;
+using System.IO;
+using XeroApi.Model;
+
+namespace XeroApi.Integration
+{
+ public class ThrottlingIntegrationProxy : IIntegrationProxy
+ {
+ private readonly IIntegrationProxy _innerIntegrationProxy;
+ private readonly IRateLimiter _rateLimiter;
+
+ public ThrottlingIntegrationProxy(IIntegrationProxy innerIntegrationProxy, IRateLimiter rateLimiter)
+ {
+ _innerIntegrationProxy = innerIntegrationProxy;
+ _rateLimiter = rateLimiter;
+ }
+
+ #region Implementation of IIntegrationProxy
+
+ public string FindElements(IApiQueryDescription apiQueryDescription)
+ {
+ EnforceRateLimit();
+ return _innerIntegrationProxy.FindElements(apiQueryDescription);
+ }
+
+ public string FindAttachments(string endpointName, string itemId)
+ {
+ EnforceRateLimit();
+ return _innerIntegrationProxy.FindAttachments(endpointName, itemId);
+ }
+
+ public byte[] FindOne(string endpointName, string itemId, string acceptMimeType)
+ {
+ EnforceRateLimit();
+ return _innerIntegrationProxy.FindOne(endpointName, itemId, acceptMimeType);
+ }
+
+ public Stream FindOneAttachment(string endpointName, string itemId, string attachmentIdOrFileName)
+ {
+ EnforceRateLimit();
+ return _innerIntegrationProxy.FindOneAttachment(endpointName, itemId, attachmentIdOrFileName);
+ }
+
+ public string UpdateOrCreateElements(string endpointName, string body)
+ {
+ EnforceRateLimit();
+ return _innerIntegrationProxy.UpdateOrCreateElements(endpointName, body);
+ }
+
+ public string UpdateOrCreateAttachment(string endpointName, string itemId, Attachment attachment)
+ {
+ EnforceRateLimit();
+ return _innerIntegrationProxy.UpdateOrCreateAttachment(endpointName, itemId, attachment);
+ }
+
+ public string CreateElements(string endpointName, string body)
+ {
+ EnforceRateLimit();
+ return _innerIntegrationProxy.CreateElements(endpointName, body);
+ }
+
+ public string CreateAttachment(string endpointName, string itemId, Attachment attachment)
+ {
+ EnforceRateLimit();
+ return _innerIntegrationProxy.CreateAttachment(endpointName, itemId, attachment);
+ }
+
+ private void EnforceRateLimit()
+ {
+ _rateLimiter.CheckAndEnforceRateLimit(DateTime.Now);
+ }
+
+ #endregion
+ }
+}
View
81 source/XeroApi/Integration/TrickleRateLimiter.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net;
+using System.Threading;
+
+namespace XeroApi.Integration
+{
+ public interface IEventTimeline
+ {
+ void RecordEvent(DateTime eventDate);
+ DateTime? GetLastEventDateAndTime();
+ }
+
+ public class EventTimeline
+ {
+
+ }
+
+
+ public interface IXXX
+ {
+ void PauseBeforeEvent(TimeSpan pauseTime);
+ }
+
+ /// <summary>
+ ///
+ /// </summary>
+ /// <remarks>
+ /// Defaults to 60 calls in a sliding 60 second time window.
+ /// </remarks>
+ public class TrickleRateLimiter : IRateLimiter
+ {
+ private IEventTimeline Timeline { get; set; }
+ private readonly IXXX _xxx;
+
+ public TrickleRateLimiter(IEventTimeline timeline)
+ {
+ Timeline = timeline;
+ }
+
+ public TrickleRateLimiter(IEventTimeline timeline, IXXX xxx)
+ {
+ Timeline = timeline;
+ _xxx = xxx;
+ }
+
+ public void CheckAndEnforceRateLimit(DateTime eventTimestamp)
+ {
+ DateTime? lastEventTimestamp = Timeline.GetLastEventDateAndTime();
+
+ if (lastEventTimestamp.HasValue)
+ {
+ TimeSpan timeSinceLastEvent = eventTimestamp - lastEventTimestamp.Value;
+
+ PauseMaybe(timeSinceLastEvent);
+ }
+
+ Timeline.RecordEvent(eventTimestamp);
+ }
+
+ private void PauseMaybe(TimeSpan timeSinceLastEvent)
+ {
+ if (timeSinceLastEvent < TimeSpan.FromSeconds(1))
+ {
+ TimeSpan timeToPause = TimeSpan.FromSeconds(1).Subtract(timeSinceLastEvent);
+ PauseFor(timeToPause);
+ }
+ }
+
+ private void PauseFor(TimeSpan timeToPause)
+ {
+ _xxx.PauseBeforeEvent(timeToPause);
+ }
+ }
+
+ public class RateLimitExceededException : Exception
+ {
+ }
+}
View
4 source/XeroApi/XeroApi.csproj
@@ -42,6 +42,10 @@
<ItemGroup>
<Compile Include="AttachmentRepository.cs" />
<Compile Include="Integration\IApiQueryDescription.cs" />
+ <Compile Include="Integration\IRateLimiter.cs" />
+ <Compile Include="Integration\NullRateLimiter.cs" />
+ <Compile Include="Integration\SimpleRateLimiter.cs" />
+ <Compile Include="Integration\ThrottlingIntegrationProxy.cs" />
<Compile Include="Linq\ApiQuery.cs" />
<Compile Include="Linq\LinqQueryDescription.cs" />
<Compile Include="Linq\ApiQueryProvider.cs" />

0 comments on commit a38c766

Please sign in to comment.