Skip to content

Commit

Permalink
Add an application cache for REST API (#9626)
Browse files Browse the repository at this point in the history
* implement application cache
* add snyk exception

---------

Signed-off-by: Jesse Nelson <[email protected]>
  • Loading branch information
jnels124 authored Nov 1, 2024
1 parent cb30c52 commit 0e2fa17
Show file tree
Hide file tree
Showing 18 changed files with 662 additions and 31 deletions.
1 change: 1 addition & 0 deletions .snyk
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ exclude:
global:
- hedera-mirror-monitor/src/main/java/com/hedera/mirror/monitor/OperatorProperties.java
- hedera-mirror-rest/tokens.js
- hedera-mirror-rest/middleware/responseCacheHandler.js
4 changes: 2 additions & 2 deletions charts/hedera-mirror/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,10 @@ redis:
resources:
limits:
cpu: 1500m
memory: 1000Mi
memory: 2000Mi
requests:
cpu: 250m
memory: 500Mi
memory: 1000Mi
sentinel:
enabled: true
masterSet: mirror
Expand Down
4 changes: 3 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,10 +503,12 @@ The following table lists the available properties along with their default valu
value, it is recommended to only populate overridden properties in the custom `application.yml`.

| Name | Default | Description |
| ------------------------------------------------------------------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|--------------------------------------------------------------------| ----------------------- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `hedera.mirror.rest.cache.entityId.maxAge` | 1800 | The number of seconds until the entityId cache entry expires |
| `hedera.mirror.rest.cache.entityId.maxSize` | 100000 | The maximum number of entries in the entityId cache |
| `hedera.mirror.rest.cache.token.maxSize` | 100000 | The maximum number of entries in the token cache |
| `hedera.mirror.rest.cache.response.enabled` | false | Whether or not the Redis based REST API response cache is enabled. If so, Redis itself must be enabled and properly configured. |
| `hedera.mirror.rest.cache.response.compress` | true | Store cached response in gzip format and serve gzipped if supported by client |
| `hedera.mirror.rest.db.host` | 127.0.0.1 | The IP or hostname used to connect to the database |
| `hedera.mirror.rest.db.name` | mirror_node | The name of the database |
| `hedera.mirror.rest.db.password` | mirror_api_pass | The database password the processor uses to connect. |
Expand Down
48 changes: 48 additions & 0 deletions hedera-mirror-rest/__tests__/cache-disabled.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import config from '../config';
import {Cache} from '../cache';
import {defaultBeforeAllTimeoutMillis} from './integrationUtils';

let cache;

beforeAll(async () => {
config.redis.enabled = false;
logger.info('Redis disabled');
}, defaultBeforeAllTimeoutMillis);

beforeEach(async () => {
cache = new Cache();
});

const loader = (keys) => keys.map((key) => `v${key}`);
const keyMapper = (key) => `k${key}`;

describe('Redis disabled', () => {
test('get', async () => {
const values = await cache.get(['1', '2', '3'], loader, keyMapper);
expect(values).toEqual(['v1', 'v2', 'v3']);
});

test('getSingleWithTtl', async () => {
const key = 'myKey';
const value = await cache.getSingleWithTtl(key);
expect(value).toBeUndefined();
const setResult = await cache.setSingle(key, 5, 'someValue');
expect(setResult).toBeUndefined();
});
});
58 changes: 58 additions & 0 deletions hedera-mirror-rest/__tests__/cache-misconfigured.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import config from '../config';
import {Cache} from '../cache';
import {RedisContainer} from '@testcontainers/redis';
import {defaultBeforeAllTimeoutMillis} from './integrationUtils';

let cache;
let redisContainer;

beforeAll(async () => {
config.redis.enabled = true;
config.redis.uri = 'redis://invalid:6379';
redisContainer = await new RedisContainer().withStartupTimeout(20000).start();
logger.info('Started Redis container');
}, defaultBeforeAllTimeoutMillis);

afterAll(async () => {
await cache.stop();
await redisContainer.stop({signal: 'SIGKILL', t: 5});
logger.info('Stopped Redis container');
});

beforeEach(async () => {
cache = new Cache();
});

const loader = (keys) => keys.map((key) => `v${key}`);
const keyMapper = (key) => `k${key}`;

describe('Misconfigured Redis URL', () => {
test('get', async () => {
const values = await cache.get(['1', '2', '3'], loader, keyMapper);
expect(values).toEqual(['v1', 'v2', 'v3']);
});

test('getSingleWithTtl', async () => {
const key = 'myKey';
const value = await cache.getSingleWithTtl(key);
expect(value).toBeUndefined();
const setResult = await cache.setSingle(key, 'someValue');
expect(setResult).toBeUndefined();
});
});
37 changes: 22 additions & 15 deletions hedera-mirror-rest/__tests__/cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,29 @@
import config from '../config';
import {Cache} from '../cache';
import {RedisContainer} from '@testcontainers/redis';
import {defaultBeforeAllTimeoutMillis} from './integrationUtils.js';
import {defaultBeforeAllTimeoutMillis} from './integrationUtils';

let cache;
let redisContainer;

beforeAll(async () => {
config.redis.enabled = true;
redisContainer = await new RedisContainer().withStartupTimeout(20000).start();
config.redis.uri = `0.0.0.0:${redisContainer.getMappedPort(6379)}`;
logger.info('Started Redis container');
}, defaultBeforeAllTimeoutMillis);

afterAll(async () => {
await cache.stop();
await redisContainer.stop({signal: 'SIGKILL', t: 5});
logger.info('Stopped Redis container');
});

beforeEach(async () => {
config.redis.uri = `0.0.0.0:${redisContainer.getMappedPort(6379)}`;
cache = new Cache();
await cache.clear();
});

afterEach(async () => {
await cache.stop();
});

const loader = (keys) => keys.map((key) => `v${key}`);
const keyMapper = (key) => `k${key}`;

Expand Down Expand Up @@ -72,17 +69,27 @@ describe('get', () => {
const values = await cache.get([], loader, keyMapper);
expect(values).toEqual([]);
});
});

test('Disabled', async () => {
config.redis.enabled = false;
const values = await cache.get(['1', '2', '3'], loader, keyMapper);
expect(values).toEqual(['v1', 'v2', 'v3']);
describe('Single key get/set', () => {
test('Get undefined key', async () => {
const value = await cache.getSingleWithTtl(undefined);
expect(value).toBeUndefined();
});

test('Unable to connect', async () => {
config.redis.uri = 'redis://invalid:6379';
cache = new Cache();
const values = await cache.get(['1', '2', '3'], loader, keyMapper);
expect(values).toEqual(['v1', 'v2', 'v3']);
test('Get non-existent key', async () => {
const key = 'myKeyDoesNotExist';
const value = await cache.getSingleWithTtl(key);
expect(value).toBeUndefined();
});

test('Set and get object', async () => {
const key = 'myKey';
const objectToCache = {a: 5, b: 'some string', c: 'another string'};
const setResult = await cache.setSingle(key, 5, objectToCache);
expect(setResult).toEqual('OK');
const objectWithTtlFromCache = await cache.getSingleWithTtl(key);
expect(objectWithTtlFromCache.value).toEqual(objectToCache);
expect(objectWithTtlFromCache.ttl).toBeGreaterThan(0);
});
});
Loading

0 comments on commit 0e2fa17

Please sign in to comment.