-
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.
Implement error resilient cache wrapper (#69)
- Loading branch information
Showing
6 changed files
with
192 additions
and
1 deletion.
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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,58 @@ | ||
import {extractErrorMessage, Log, Logger, LogLevel} from '@croct/logging'; | ||
import {CacheLoader, CacheProvider} from './cacheProvider'; | ||
|
||
type CacheErrorLog = Log<{ | ||
errorMessage: string, | ||
errorStack?: string, | ||
}>; | ||
|
||
/** | ||
* A cache wrapper that prevents any error from propagating to the caller. | ||
* | ||
* Errors retrieving values from the cache behave as a cache miss. | ||
*/ | ||
export class ErrorResilientCache<K, V> implements CacheProvider<K, V> { | ||
private readonly cache: CacheProvider<K, V>; | ||
|
||
private readonly logger: Logger<CacheErrorLog>; | ||
|
||
public constructor(cache: CacheProvider<K, V>, logger: Logger<CacheErrorLog>) { | ||
this.cache = cache; | ||
this.logger = logger; | ||
|
||
this.logError = this.logError.bind(this); | ||
} | ||
|
||
public get(key: K, loader: CacheLoader<K, V>): Promise<V> { | ||
return this.cache | ||
.get(key, loader) | ||
.catch(error => { | ||
this.logError(error); | ||
|
||
return loader(key); | ||
}); | ||
} | ||
|
||
public set(key: K, value: V): Promise<void> { | ||
return this.cache | ||
.set(key, value) | ||
.catch(this.logError); | ||
} | ||
|
||
public delete(key: K): Promise<void> { | ||
return this.cache | ||
.delete(key) | ||
.catch(this.logError); | ||
} | ||
|
||
private logError(error: unknown): void { | ||
this.logger.log({ | ||
level: LogLevel.ERROR, | ||
message: 'Suppressed error on cache operation', | ||
details: { | ||
errorMessage: extractErrorMessage(error), | ||
errorStack: error instanceof Error ? error.stack : undefined, | ||
}, | ||
}); | ||
} | ||
} |
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,109 @@ | ||
import {InMemoryLogger, Log, LogLevel} from '@croct/logging'; | ||
import {CacheProvider, ErrorResilientCache, InMemoryCache} from '../src'; | ||
|
||
class FailingCache implements CacheProvider<any, any> { | ||
public delete(): Promise<void> { | ||
return Promise.reject(new Error('Failing cache implementation.')); | ||
} | ||
|
||
public get(): Promise<any> { | ||
return Promise.reject(new Error('Failing cache implementation.')); | ||
} | ||
|
||
public set(): Promise<void> { | ||
return Promise.reject(new Error('Failing cache implementation.')); | ||
} | ||
} | ||
|
||
describe('An error-resilient cache wrapper', () => { | ||
it('should forward get calls', async () => { | ||
const innerCache = new InMemoryCache(); | ||
const logger = new InMemoryLogger(); | ||
const cache = new ErrorResilientCache(innerCache, logger); | ||
|
||
await innerCache.set('foo', 'bar'); | ||
|
||
await expect(cache.get('foo', () => Promise.resolve(null))).resolves.toBe('bar'); | ||
await expect(cache.get('baz', () => Promise.resolve(null))).resolves.toBeNull(); | ||
|
||
expect(logger.getLogs()).toStrictEqual([]); | ||
}); | ||
|
||
it('should forward set calls', async () => { | ||
const innerCache = new InMemoryCache(); | ||
const logger = new InMemoryLogger(); | ||
const cache = new ErrorResilientCache(innerCache, logger); | ||
|
||
await cache.set('foo', 'bar'); | ||
|
||
await expect(cache.get('foo', () => Promise.resolve(null))).resolves.toBe('bar'); | ||
await expect(innerCache.get('foo', () => Promise.resolve(null))).resolves.toBe('bar'); | ||
|
||
expect(logger.getLogs()).toStrictEqual([]); | ||
}); | ||
|
||
it('should forward delete calls', async () => { | ||
const innerCache = new InMemoryCache(); | ||
const logger = new InMemoryLogger(); | ||
const cache = new ErrorResilientCache(innerCache, logger); | ||
|
||
await innerCache.set('foo', 'bar'); | ||
await cache.delete('foo'); | ||
|
||
await expect(cache.get('foo', () => Promise.resolve(null))).resolves.toBeNull(); | ||
await expect(innerCache.get('foo', () => Promise.resolve(null))).resolves.toBeNull(); | ||
|
||
expect(logger.getLogs()).toStrictEqual([]); | ||
}); | ||
|
||
it('should handle errors on get calls as a cache miss', async () => { | ||
const innerCache = new FailingCache(); | ||
const logger = new InMemoryLogger(); | ||
const cache = new ErrorResilientCache(innerCache, logger); | ||
|
||
await expect(cache.get('foo', () => Promise.resolve(null))).resolves.toBeNull(); | ||
|
||
expect(logger.getLogs()).toStrictEqual<Log[]>([{ | ||
level: LogLevel.ERROR, | ||
message: 'Suppressed error on cache operation', | ||
details: { | ||
errorMessage: 'Failing cache implementation.', | ||
errorStack: expect.stringContaining('at FailingCache.get'), | ||
}, | ||
}]); | ||
}); | ||
|
||
it('should suppress errors on set calls', async () => { | ||
const innerCache = new FailingCache(); | ||
const logger = new InMemoryLogger(); | ||
const cache = new ErrorResilientCache(innerCache, logger); | ||
|
||
await cache.set('foo', 'bar'); | ||
|
||
expect(logger.getLogs()).toStrictEqual<Log[]>([{ | ||
level: LogLevel.ERROR, | ||
message: 'Suppressed error on cache operation', | ||
details: { | ||
errorMessage: 'Failing cache implementation.', | ||
errorStack: expect.stringContaining('at FailingCache.set'), | ||
}, | ||
}]); | ||
}); | ||
|
||
it('should suppress errors on delete calls', async () => { | ||
const innerCache = new FailingCache(); | ||
const logger = new InMemoryLogger(); | ||
const cache = new ErrorResilientCache(innerCache, logger); | ||
|
||
await cache.delete('foo'); | ||
|
||
expect(logger.getLogs()).toStrictEqual<Log[]>([{ | ||
level: LogLevel.ERROR, | ||
message: 'Suppressed error on cache operation', | ||
details: { | ||
errorMessage: 'Failing cache implementation.', | ||
errorStack: expect.stringContaining('at FailingCache.delete'), | ||
}, | ||
}]); | ||
}); | ||
}); |