-
Notifications
You must be signed in to change notification settings - Fork 241
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CLDSRV-574 implement KMS health check
- Loading branch information
1 parent
15f5866
commit 4352e97
Showing
5 changed files
with
374 additions
and
20 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
class Cache { | ||
constructor() { | ||
this.lastChecked = null; | ||
this.result = null; | ||
} | ||
|
||
/** | ||
* Retrieves the cached result with the last checked timestamp. | ||
* @returns {object|null} An object containing the result and lastChecked, or null if not set. | ||
*/ | ||
getResult() { | ||
if (!this.result) { | ||
return null; | ||
} | ||
|
||
return Object.assign({}, this.result, { | ||
lastChecked: this.lastChecked ? new Date(this.lastChecked).toISOString() : null, | ||
}); | ||
} | ||
|
||
/** | ||
* Retrieves the last checked timestamp. | ||
* @returns {number|null} The timestamp of the last check or null if never checked. | ||
*/ | ||
getLastChecked() { | ||
return this.lastChecked; | ||
} | ||
|
||
/** | ||
* Updates the cache with a new result and timestamp. | ||
* @param {object} result - The result to cache. | ||
* @returns {undefined} | ||
*/ | ||
setResult(result) { | ||
this.lastChecked = Date.now(); | ||
this.result = result; | ||
} | ||
|
||
/** | ||
* Determines if the cache should be refreshed based on the last checked time. | ||
* @param {number} duration - Duration in milliseconds for cache validity. | ||
* @returns {boolean} true if the cache should be refreshed, else false. | ||
*/ | ||
shouldRefresh(duration = 1 * 60 * 60 * 1000) { // Default: 1 hour | ||
if (!this.lastChecked) { | ||
return true; | ||
} | ||
|
||
const now = Date.now(); | ||
const elapsed = now - this.lastChecked; | ||
const jitter = Math.floor(Math.random() * 15 * 60 * 1000); // Up to 15 minutes | ||
return elapsed > (duration - jitter); | ||
} | ||
|
||
/** | ||
* Clears the cache. | ||
* @returns {undefined} | ||
*/ | ||
clear() { | ||
this.lastChecked = null; | ||
this.result = null; | ||
} | ||
} | ||
|
||
module.exports = Cache; |
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
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,132 @@ | ||
const assert = require('assert'); | ||
const sinon = require('sinon'); | ||
const { errors } = require('arsenal'); | ||
|
||
const Cache = require('../../../lib/kms/Cache'); | ||
const { DummyRequestLogger } = require('../helpers'); | ||
const memBackend = require('../../../lib/kms/in_memory/backend'); | ||
const kms = require('../../../lib/kms/wrapper'); | ||
|
||
const log = new DummyRequestLogger(); | ||
|
||
describe('KMS.checkHealth', () => { | ||
let setResultSpy; | ||
let shouldRefreshStub; | ||
let clock; | ||
|
||
beforeEach(() => { | ||
clock = sinon.useFakeTimers({ | ||
now: 1625077800000, | ||
toFake: ['Date'], | ||
}); | ||
|
||
setResultSpy = sinon.spy(Cache.prototype, 'setResult'); | ||
shouldRefreshStub = sinon.stub(Cache.prototype, 'shouldRefresh').returns(true); | ||
|
||
delete memBackend.backend.healthcheck; | ||
}); | ||
|
||
afterEach(() => { | ||
sinon.restore(); | ||
if (clock) { | ||
clock.restore(); | ||
} | ||
}); | ||
|
||
it('should return OK when kms backend does not have healthcheck method', done => { | ||
kms.checkHealth(log, (err, result) => { | ||
assert.ifError(err); | ||
assert.deepStrictEqual(result, { | ||
memoryKms: { code: 200, message: 'OK' }, | ||
}); | ||
|
||
assert(shouldRefreshStub.notCalled, 'shouldRefresh should not be called'); | ||
assert(setResultSpy.notCalled, 'setResult should not be called'); | ||
|
||
done(); | ||
}); | ||
}); | ||
|
||
it('should return OK when healthcheck succeeds', done => { | ||
memBackend.backend.healthcheck = sinon.stub().callsFake((log, cb) => cb(null)); | ||
|
||
kms.checkHealth(log, (err, result) => { | ||
assert.ifError(err); | ||
|
||
const expectedLastChecked = new Date(clock.now).toISOString(); | ||
|
||
assert.deepStrictEqual(result, { | ||
memoryKms: { code: 200, message: 'OK', lastChecked: expectedLastChecked }, | ||
}); | ||
|
||
assert(shouldRefreshStub.calledOnce, 'shouldRefresh should be called once'); | ||
|
||
assert(setResultSpy.calledOnceWithExactly({ | ||
code: 200, | ||
message: 'OK', | ||
})); | ||
|
||
done(); | ||
}); | ||
}); | ||
|
||
it('should return failure message when healthcheck fails', done => { | ||
memBackend.backend.healthcheck = sinon.stub().callsFake((log, cb) => cb(errors.InternalError)); | ||
|
||
kms.checkHealth(log, (err, result) => { | ||
assert.ifError(err); | ||
|
||
const expectedLastChecked = new Date(clock.now).toISOString(); | ||
|
||
assert.deepStrictEqual(result, { | ||
memoryKms: { | ||
code: 500, | ||
message: 'KMS health check failed', | ||
description: 'We encountered an internal error. Please try again.', | ||
lastChecked: expectedLastChecked, | ||
}, | ||
}); | ||
|
||
assert(shouldRefreshStub.calledOnce, 'shouldRefresh should be called once'); | ||
|
||
assert(setResultSpy.calledOnceWithExactly({ | ||
code: 500, | ||
message: 'KMS health check failed', | ||
description: 'We encountered an internal error. Please try again.', | ||
})); | ||
|
||
done(); | ||
}); | ||
}); | ||
|
||
it('should use cached result when not refreshing', done => { | ||
memBackend.backend.healthcheck = sinon.stub().callsFake((log, cb) => cb(null)); | ||
// first call to populate the cache | ||
kms.checkHealth(log, err => { | ||
assert.ifError(err); | ||
shouldRefreshStub.returns(false); | ||
|
||
// second call should use the cached result | ||
kms.checkHealth(log, (err, result) => { | ||
assert.ifError(err); | ||
|
||
const expectedLastChecked = new Date(clock.now).toISOString(); | ||
assert.deepStrictEqual(result, { | ||
memoryKms: { | ||
code: 200, | ||
message: 'OK', | ||
lastChecked: expectedLastChecked, | ||
}, | ||
}); | ||
|
||
// once each call | ||
assert.strictEqual(shouldRefreshStub.callCount, 2, 'shouldRefresh should be called twice'); | ||
|
||
// only the first call | ||
assert.strictEqual(setResultSpy.callCount, 1, 'setResult should be called once'); | ||
|
||
done(); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.