-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from Rohland/avoid-sumo-429
Add rate limiting to Sumo Logic calls
- Loading branch information
Showing
5 changed files
with
310 additions
and
10 deletions.
There are no files selected for viewing
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 | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { executeSumoRequest } from "./sumo"; | ||
|
||
describe('sumo ', () => { | ||
describe('executeSumoRequest', () => { | ||
describe("when executed with success", () => { | ||
it("should return result", async () => { | ||
// arrange | ||
const request = jest.fn().mockResolvedValue("result"); | ||
|
||
// act | ||
const result = await executeSumoRequest("test", request); | ||
|
||
// assert | ||
expect(result).toEqual("result"); | ||
expect(request).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
describe("when failure", () => { | ||
it("should throw error", async () => { | ||
// arrange | ||
const request = jest.fn().mockRejectedValue(new Error("error")); | ||
|
||
// act | ||
let error; | ||
try { | ||
await executeSumoRequest("test", request); | ||
} catch (err) { | ||
error = err; | ||
} | ||
|
||
// assert | ||
expect(error).toEqual(new Error("error")); | ||
expect(request).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
describe("when request takes some time", () => { | ||
it("should wait", async () => { | ||
// arrange | ||
const request = jest.fn().mockImplementation(() => new Promise(resolve => setTimeout(() => resolve("result"), 100))); | ||
|
||
// act | ||
const start = performance.now(); | ||
const result = await executeSumoRequest("test", request); | ||
const end = performance.now(); | ||
|
||
// assert | ||
expect(result).toEqual("result"); | ||
expect(end - start).toBeGreaterThanOrEqual(95); | ||
expect(request).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
describe("when multiple requests sent", () => { | ||
it("should queue them and only execute 5 per second", async () => { | ||
// arrange | ||
const count = 20; | ||
const requests = []; | ||
const countPerSecond = new Map<number, number>(); | ||
for (let i = 0; i < count; i++) { | ||
const req = jest.fn().mockImplementation(() => new Promise(resolve => { | ||
const time = Math.round(performance.now() / 1000); | ||
const count = countPerSecond.get(time) ?? 0; | ||
countPerSecond.set(time, count + 1); | ||
setTimeout(() => resolve(`result${ i }`), 100) | ||
})); | ||
requests.push(req); | ||
} | ||
|
||
// act | ||
const start = performance.now(); | ||
const result = await Promise.all(requests.map(x => executeSumoRequest("test", x))); | ||
const end = performance.now(); | ||
|
||
// assert | ||
expect(result).toEqual(requests.map((_x,i) => `result${ i }`)); | ||
expect(end - start).toBeGreaterThanOrEqual(2000); | ||
expect(end - start).toBeLessThanOrEqual(4000); | ||
requests.forEach(x => expect(x).toHaveBeenCalledTimes(1)); | ||
countPerSecond.forEach((value, _) => { | ||
expect(value).toBeLessThanOrEqual(5); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
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 | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { RateLimiter } from "./rate-limiter"; | ||
|
||
describe('rate-limiter', () => { | ||
describe('execute', () => { | ||
describe("when executed with success", () => { | ||
it("should return result", async () => { | ||
// arrange | ||
const request = jest.fn().mockResolvedValue("result"); | ||
const sut = getSut();; | ||
|
||
// act | ||
const result = await sut.execute(request); | ||
|
||
// assert | ||
expect(result).toEqual("result"); | ||
expect(request).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
describe("when failure", () => { | ||
it("should throw error", async () => { | ||
// arrange | ||
const request = jest.fn().mockRejectedValue(new Error("error")); | ||
const sut = getSut();; | ||
|
||
// act | ||
let error; | ||
try { | ||
await sut.execute(request); | ||
} catch (err) { | ||
error = err; | ||
} | ||
|
||
// assert | ||
expect(error).toEqual(new Error("error")); | ||
expect(request).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
describe("when request takes some time", () => { | ||
it("should wait", async () => { | ||
// arrange | ||
const sut = getSut();; | ||
const request = jest.fn().mockImplementation(() => new Promise(resolve => setTimeout(() => resolve("result"), 100))); | ||
|
||
// act | ||
const start = performance.now(); | ||
const result = await sut.execute(request); | ||
const end = performance.now(); | ||
|
||
// assert | ||
expect(result).toEqual("result"); | ||
expect(end - start).toBeGreaterThanOrEqual(95); | ||
expect(request).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
describe("when multiple requests sent", () => { | ||
it("should queue them only execute x in parallel", async () => { | ||
const maxPerSec = 100; | ||
const maxConcurrent = 1; | ||
const sut = getSut(maxPerSec, maxConcurrent); | ||
const count = 3; | ||
const requests = []; | ||
for (let i = 0; i < count; i++) { | ||
const req = jest.fn().mockImplementation(() => new Promise(resolve => { | ||
setTimeout(() => resolve(`result${ i }`), 500) | ||
})); | ||
requests.push(req); | ||
} | ||
|
||
// act | ||
const start = performance.now(); | ||
const result = await Promise.all(requests.map(x => sut.execute(x))); | ||
const end = performance.now(); | ||
|
||
// assert | ||
expect(result).toEqual(requests.map((_x,i) => `result${ i }`)); | ||
expect(end - start).toBeGreaterThanOrEqual(1500); | ||
requests.forEach(x => expect(x).toHaveBeenCalledTimes(1)); | ||
}); | ||
it("should queue them and only execute x per second", async () => { | ||
// arrange | ||
const maxPerSec = 5; | ||
const sut = getSut(maxPerSec); | ||
const count = 20; | ||
const requests = []; | ||
const countPerSecond = new Map<number, number>(); | ||
for (let i = 0; i < count; i++) { | ||
const req = jest.fn().mockImplementation(() => new Promise(resolve => { | ||
const time = Math.round(performance.now() / 1000); | ||
const count = countPerSecond.get(time) ?? 0; | ||
countPerSecond.set(time, count + 1); | ||
setTimeout(() => resolve(`result${ i }`), 100) | ||
})); | ||
requests.push(req); | ||
} | ||
|
||
// act | ||
const start = performance.now(); | ||
const result = await Promise.all(requests.map(x => sut.execute(x))); | ||
const end = performance.now(); | ||
|
||
// assert | ||
expect(result).toEqual(requests.map((_x,i) => `result${ i }`)); | ||
expect(end - start).toBeGreaterThanOrEqual(2000); | ||
expect(end - start).toBeLessThanOrEqual(4000); | ||
requests.forEach(x => expect(x).toHaveBeenCalledTimes(1)); | ||
countPerSecond.forEach((value, _) => { | ||
expect(value).toBeLessThanOrEqual(maxPerSec); | ||
}); | ||
}); | ||
}); | ||
}); | ||
function getSut(perSecond = null, concurrent = null) { | ||
return new RateLimiter(perSecond ?? 5, concurrent ?? 3); | ||
} | ||
}); |
Oops, something went wrong.