Skip to content
This repository has been archived by the owner on Nov 10, 2020. It is now read-only.

Commit

Permalink
Add TrickleRateLimiter ThrottlingIntegrationProxy (and tests)
Browse files Browse the repository at this point in the history
  • Loading branch information
danbarratt committed Nov 17, 2011
1 parent a01eac2 commit a38c766
Show file tree
Hide file tree
Showing 9 changed files with 439 additions and 0 deletions.
Binary file added libraries/Rhino.Mocks.dll
Binary file not shown.
65 changes: 65 additions & 0 deletions source/XeroApi.Tests/ThrottlingIntegrationProxyTests.cs
Original file line number Original file line Diff line number Diff line change
@@ -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));
}
}
}
176 changes: 176 additions & 0 deletions source/XeroApi.Tests/TrickleRateLimiterTests.cs
Original file line number Original file line Diff line number Diff line change
@@ -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();
}
}
}
5 changes: 5 additions & 0 deletions source/XeroApi.Tests/XeroApi.Tests.csproj
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
<Reference Include="nunit.framework"> <Reference Include="nunit.framework">
<HintPath>..\..\libraries\nunit.framework.dll</HintPath> <HintPath>..\..\libraries\nunit.framework.dll</HintPath>
</Reference> </Reference>
<Reference Include="Rhino.Mocks">
<HintPath>..\..\libraries\Rhino.Mocks.dll</HintPath>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
Expand All @@ -45,8 +48,10 @@
<Compile Include="IntegrationProxyHelperTests.cs" /> <Compile Include="IntegrationProxyHelperTests.cs" />
<Compile Include="ModelSerializationTests.cs" /> <Compile Include="ModelSerializationTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SimpleRateLimiterTests.cs" />
<Compile Include="Stubs\StubContact.cs" /> <Compile Include="Stubs\StubContact.cs" />
<Compile Include="Stubs\StubIntegrationProxy.cs" /> <Compile Include="Stubs\StubIntegrationProxy.cs" />
<Compile Include="ThrottlingIntegrationProxyTests.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\XeroApi\XeroApi.csproj"> <ProjectReference Include="..\XeroApi\XeroApi.csproj">
Expand Down
15 changes: 15 additions & 0 deletions source/XeroApi/Integration/IRateLimiter.cs
Original file line number Original file line Diff line number Diff line change
@@ -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);

}
}
18 changes: 18 additions & 0 deletions source/XeroApi/Integration/NullRateLimiter.cs
Original file line number Original file line Diff line number Diff line change
@@ -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)
{
}
}
}
75 changes: 75 additions & 0 deletions source/XeroApi/Integration/ThrottlingIntegrationProxy.cs
Original file line number Original file line Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit a38c766

Please sign in to comment.