Skip to content

Commit

Permalink
Merge pull request apollographql#2109 from apollographql/abernix/cach…
Browse files Browse the repository at this point in the history
…ing-tweak-prep-two

Improve InMemoryLRUCache implementation.
  • Loading branch information
abernix authored Dec 18, 2018
2 parents 8ad2bdd + fa63116 commit 8a1f999
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
jest.mock('memcached', () => require('memcached-mock'));

import { MemcachedCache } from '../index';
import { testKeyValueCache } from '../../../apollo-server-caching/src/__tests__/testsuite';
import {
testKeyValueCache_Basics,
testKeyValueCache_Expiration,
} from '../../../apollo-server-caching/src/__tests__/testsuite';

testKeyValueCache(new MemcachedCache('localhost'));
describe('Memcached', () => {
const cache = new MemcachedCache('localhost');
testKeyValueCache_Basics(cache);
testKeyValueCache_Expiration(cache);
});
4 changes: 2 additions & 2 deletions packages/apollo-server-cache-memcached/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ export class MemcachedCache implements KeyValueCache {

async set(
key: string,
data: string,
value: string,
options?: { ttl?: number },
): Promise<void> {
const { ttl } = Object.assign({}, this.defaultSetOptions, options);
await this.client.set(key, data, ttl);
await this.client.set(key, value, ttl);
}

async get(key: string): Promise<string | undefined> {
Expand Down
11 changes: 9 additions & 2 deletions packages/apollo-server-cache-redis/src/__tests__/Redis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ jest.mock('redis', () => require('redis-mock'));
jest.useFakeTimers(); // mocks out setTimeout that is used in redis-mock

import { RedisCache } from '../index';
import { testKeyValueCache } from '../../../apollo-server-caching/src/__tests__/testsuite';
import {
testKeyValueCache_Basics,
testKeyValueCache_Expiration,
} from '../../../apollo-server-caching/src/__tests__/testsuite';

testKeyValueCache(new RedisCache({ host: 'localhost' }));
describe('Redis', () => {
const cache = new RedisCache({ host: 'localhost' });
testKeyValueCache_Basics(cache);
testKeyValueCache_Expiration(cache);
});
6 changes: 3 additions & 3 deletions packages/apollo-server-cache-redis/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Redis from 'redis';
import { promisify } from 'util';
import DataLoader from 'dataloader';

export class RedisCache implements KeyValueCache {
export class RedisCache implements KeyValueCache<string> {
// FIXME: Replace any with proper promisified type
readonly client: any;
readonly defaultSetOptions = {
Expand Down Expand Up @@ -31,11 +31,11 @@ export class RedisCache implements KeyValueCache {

async set(
key: string,
data: string,
value: string,
options?: { ttl?: number },
): Promise<void> {
const { ttl } = Object.assign({}, this.defaultSetOptions, options);
await this.client.set(key, data, 'EX', ttl);
await this.client.set(key, value, 'EX', ttl);
}

async get(key: string): Promise<string | undefined> {
Expand Down
15 changes: 14 additions & 1 deletion packages/apollo-server-caching/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export interface KeyValueCache {
}
```

## Running test suite
## Testing cache implementations

### Test helpers

You can export and run a jest test suite from `apollo-server-caching` to test your implementation:

Expand All @@ -28,4 +30,15 @@ import { testKeyValueCache } from 'apollo-server-caching';
testKeyValueCache(new MemcachedCache('localhost'));
```

The default `testKeyValueCache` helper will run all key-value store tests on the specified store, including basic `get` and `set` functionality, along with time-based expunging rules.

Some key-value cache implementations may not be able to support the full suite of tests (for example, some tests might not be able to expire based on time). For those cases, there are more granular implementations which can be used:

* `testKeyValueCache_Basic`
* `testKeyValueCache_Expiration`

For more details, consult the [source for `apollo-server-caching`](./src/__tests__/testsuite.ts).

### Running tests

Run tests with `jest --verbose`
32 changes: 27 additions & 5 deletions packages/apollo-server-caching/src/InMemoryLRUCache.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import LRU from 'lru-cache';
import { KeyValueCache } from './KeyValueCache';

export class InMemoryLRUCache implements KeyValueCache {
private store: LRU.Cache<string, string>;
export class InMemoryLRUCache<V = string> implements KeyValueCache<V> {
private store: LRU.Cache<string, V>;

// FIXME: Define reasonable default max size of the cache
constructor({ maxSize = Infinity }: { maxSize?: number } = {}) {
this.store = new LRU({
max: maxSize,
length: item => item.length,
length(item) {
if (Array.isArray(item) || typeof item === 'string') {
return item.length;
}

// If it's an object, we'll use the length to get an approximate,
// relative size of what it would take to store it. It's certainly not
// 100% accurate, but it's a very, very fast implementation and it
// doesn't require bringing in other dependencies or logic which we need
// to maintain. In the future, we might consider something like:
// npm.im/object-sizeof, but this should be sufficient for now.
if (typeof item === 'object') {
return JSON.stringify(item).length;
}

// Go with the lru-cache default "naive" size, in lieu anything better:
// https://github.com/isaacs/node-lru-cache/blob/a71be6cd/index.js#L17
return 1;
},
});
}

async get(key: string) {
return this.store.get(key);
}
async set(key: string, value: string) {
this.store.set(key, value);
async set(key: string, value: V, options?: { ttl?: number }) {
const maxAge = options && options.ttl && options.ttl * 1000;
this.store.set(key, value, maxAge);
}
async delete(key: string) {
this.store.del(key);
}
async flush(): Promise<void> {
this.store.reset();
}
}
6 changes: 3 additions & 3 deletions packages/apollo-server-caching/src/KeyValueCache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface KeyValueCache {
get(key: string): Promise<string | undefined>;
set(key: string, value: string, options?: { ttl?: number }): Promise<void>;
export interface KeyValueCache<V = string> {
get(key: string): Promise<V | undefined>;
set(key: string, value: V, options?: { ttl?: number }): Promise<void>;
delete(key: string): Promise<boolean | void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {
testKeyValueCache_Basics,
testKeyValueCache_Expiration,
} from '../../../apollo-server-caching/src/__tests__/testsuite';
import { InMemoryLRUCache } from '../InMemoryLRUCache';

describe('InMemoryLRUCache', () => {
const cache = new InMemoryLRUCache();
testKeyValueCache_Basics(cache);
testKeyValueCache_Expiration(cache);
});
39 changes: 27 additions & 12 deletions packages/apollo-server-caching/src/__tests__/testsuite.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import { advanceTimeBy, mockDate, unmockDate } from '__mocks__/date';

export function testKeyValueCache(keyValueCache: any) {
describe('KeyValueCache Test Suite', () => {
beforeAll(() => {
mockDate();
jest.useFakeTimers();
});

export function testKeyValueCache_Basics(keyValueCache: any) {
describe('basic cache functionality', () => {
beforeEach(() => {
keyValueCache.flush();
});

afterAll(() => {
unmockDate();
keyValueCache.close();
});

it('can do a basic get and set', async () => {
await keyValueCache.set('hello', 'world');
expect(await keyValueCache.get('hello')).toBe('world');
Expand All @@ -28,6 +18,24 @@ export function testKeyValueCache(keyValueCache: any) {
await keyValueCache.delete('hello');
expect(await keyValueCache.get('hello')).toBeUndefined();
});
});
}

export function testKeyValueCache_Expiration(keyValueCache: any) {
describe('time-based cache expunging', () => {
beforeAll(() => {
mockDate();
jest.useFakeTimers();
});

beforeEach(() => {
keyValueCache.flush();
});

afterAll(() => {
unmockDate();
keyValueCache.close();
});

it('is able to expire keys based on ttl', async () => {
await keyValueCache.set('short', 's', { ttl: 1 });
Expand All @@ -45,3 +53,10 @@ export function testKeyValueCache(keyValueCache: any) {
});
});
}

export function testKeyValueCache(keyValueCache: any) {
describe('KeyValueCache Test Suite', () => {
testKeyValueCache_Basics(keyValueCache);
testKeyValueCache_Expiration(keyValueCache);
});
}

0 comments on commit 8a1f999

Please sign in to comment.