This repository has been archived by the owner on Nov 10, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add TrickleRateLimiter ThrottlingIntegrationProxy (and tests)
- Loading branch information
1 parent
a01eac2
commit a38c766
Showing
9 changed files
with
439 additions
and
0 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |||
} | |||
} | |||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |||
} | |||
} | |||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |||
|
|||
} | |||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |||
{ | |||
} | |||
} | |||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |||
} | |||
} |
Oops, something went wrong.