Skip to content

Commit 54e845e

Browse files
authored
Merge pull request #1459 from input-output-hk/feat/cache_health_check
feat(cardano-services): improve health check response times by aggregating and caching
2 parents 85c38e2 + 2bc68ea commit 54e845e

File tree

17 files changed

+504
-103
lines changed

17 files changed

+504
-103
lines changed

packages/cardano-services/src/Asset/TypeormAssetProvider/TypeormAssetProvider.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ export class TypeormAssetProvider extends TypeormProvider implements AssetProvid
2828
#paginationPageSizeLimit: number;
2929

3030
constructor({ paginationPageSizeLimit }: TypeormAssetProviderProps, dependencies: TypeormAssetProviderDependencies) {
31-
const { connectionConfig$, entities, logger } = dependencies;
32-
super('TypeormAssetProvider', { connectionConfig$, entities, logger });
31+
const { connectionConfig$, entities, logger, healthCheckCache } = dependencies;
32+
super('TypeormAssetProvider', { connectionConfig$, entities, healthCheckCache, logger });
3333

3434
this.#dependencies = dependencies;
3535
this.#paginationPageSizeLimit = paginationPageSizeLimit;
36-
this.#nftMetadataService = new TypeOrmNftMetadataService({ connectionConfig$, entities, logger });
36+
this.#nftMetadataService = new TypeOrmNftMetadataService({ connectionConfig$, entities, healthCheckCache, logger });
3737
}
3838

3939
async getAsset({ assetId, extraData }: GetAssetArgs): Promise<Asset.AssetInfo> {

packages/cardano-services/src/InMemoryCache/InMemoryCache.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ export type Key = string | number;
55
export type AsyncAction<T> = () => Promise<T>;
66

77
export class InMemoryCache {
8-
#cache: NodeCache;
9-
#ttlDefault: number;
8+
protected cache: NodeCache;
9+
protected ttlDefault: number;
1010

1111
/**
1212
*
1313
* @param ttl The default time to live in seconds
1414
* @param cache The cache engine. It must extend NodeCache
1515
*/
1616
constructor(ttl: Seconds | 0, cache: NodeCache = new NodeCache()) {
17-
this.#ttlDefault = ttl;
18-
this.#cache = cache;
17+
this.ttlDefault = ttl;
18+
this.cache = cache;
1919
}
2020

2121
/**
@@ -26,16 +26,16 @@ export class InMemoryCache {
2626
* @param ttl The time to live in seconds
2727
* @returns The value stored with the key
2828
*/
29-
public async get<T>(key: Key, asyncAction: AsyncAction<T>, ttl = this.#ttlDefault): Promise<T> {
30-
const cachedValue: T | undefined = this.#cache.get(key);
29+
public async get<T>(key: Key, asyncAction: AsyncAction<T>, ttl = this.ttlDefault): Promise<T> {
30+
const cachedValue: T | undefined = this.cache.get(key);
3131
if (cachedValue) {
3232
return cachedValue;
3333
}
3434

3535
const resultPromise = asyncAction();
3636
this.set(
3737
key,
38-
resultPromise.catch(() => this.#cache.del(key)),
38+
resultPromise.catch(() => this.cache.del(key)),
3939
ttl
4040
);
4141
return resultPromise;
@@ -48,7 +48,7 @@ export class InMemoryCache {
4848
* @returns The value stored in the key
4949
*/
5050
public getVal<T>(key: Key) {
51-
return this.#cache.get<T>(key);
51+
return this.cache.get<T>(key);
5252
}
5353

5454
/**
@@ -59,8 +59,8 @@ export class InMemoryCache {
5959
* @param ttl The time to live in seconds
6060
* @returns The success state of the operation
6161
*/
62-
public set<T>(key: Key, value: T, ttl = this.#ttlDefault) {
63-
return this.#cache.set<T>(key, value, ttl);
62+
public set<T>(key: Key, value: T, ttl = this.ttlDefault) {
63+
return this.cache.set<T>(key, value, ttl);
6464
}
6565

6666
/**
@@ -69,7 +69,7 @@ export class InMemoryCache {
6969
* @param keys cache key to delete or a array of cache keys
7070
*/
7171
public invalidate(keys: Key | Key[]) {
72-
this.#cache.del(keys);
72+
this.cache.del(keys);
7373
}
7474

7575
/**
@@ -78,16 +78,16 @@ export class InMemoryCache {
7878
* @returns An array of all keys
7979
*/
8080
public keys() {
81-
return this.#cache.keys();
81+
return this.cache.keys();
8282
}
8383

8484
/** Clear the interval timeout which is set on check period option. Default: 600 */
8585
public shutdown() {
86-
this.#cache.close();
86+
this.cache.close();
8787
}
8888

8989
/** Clear the whole data and reset the stats */
9090
public clear() {
91-
this.#cache.flushAll();
91+
this.cache.flushAll();
9292
}
9393
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { AsyncAction, InMemoryCache, Key } from './InMemoryCache';
2+
import { Seconds } from '@cardano-sdk/core';
3+
import NodeCache from 'node-cache';
4+
5+
interface WarmCacheItem<T> {
6+
asyncAction: AsyncAction<T>;
7+
value: T;
8+
ttl: number;
9+
updateTime: number;
10+
}
11+
12+
export class WarmCache extends InMemoryCache {
13+
constructor(ttl: Seconds, expireCheckPeriod: Seconds) {
14+
const cache = new NodeCache({
15+
checkperiod: expireCheckPeriod,
16+
deleteOnExpire: true,
17+
stdTTL: ttl,
18+
useClones: false
19+
});
20+
super(ttl, cache);
21+
22+
this.cache.on('expired', (key, value) => {
23+
this.#warm(key, value, this.cache);
24+
});
25+
}
26+
27+
public mockCache(cache: NodeCache) {
28+
this.cache = cache;
29+
}
30+
public async get<T>(key: Key, asyncAction: AsyncAction<T>, ttl = this.ttlDefault): Promise<T> {
31+
const cachedValue: WarmCacheItem<T> | undefined = this.cache.get(key);
32+
33+
if (cachedValue && cachedValue.value) {
34+
return cachedValue.value;
35+
}
36+
37+
const updateTime = Date.now();
38+
const promise = this.#setWarmCacheItem<T>(key, asyncAction, ttl, this.cache, updateTime);
39+
40+
this.cache.set(
41+
key,
42+
{
43+
asyncAction,
44+
ttl,
45+
updateTime,
46+
value: promise
47+
// value: _resolved ? Promise.resolve(value) : Promise.reject(value)
48+
} as WarmCacheItem<T>,
49+
ttl
50+
);
51+
52+
return promise;
53+
}
54+
55+
#warm<T>(key: string, item: WarmCacheItem<T> | undefined, cacheNode: NodeCache) {
56+
if (item && item.asyncAction) {
57+
this.#setWarmCacheItem(key, item.asyncAction, item.ttl, cacheNode, Date.now()).catch(
58+
() => 'rejected in the background'
59+
);
60+
}
61+
}
62+
63+
async #setWarmCacheItem<T>(
64+
key: Key,
65+
asyncAction: AsyncAction<T>,
66+
ttl: number,
67+
cacheNode: NodeCache,
68+
updateTime: number
69+
) {
70+
const handleValue = (value: T, _resolved = true) => {
71+
const item = this.cache.get(key) as WarmCacheItem<T>;
72+
if (item && item.updateTime > updateTime) {
73+
return item.value;
74+
}
75+
const promise = _resolved ? Promise.resolve(value) : Promise.reject(value);
76+
77+
cacheNode.set(
78+
key,
79+
{
80+
asyncAction,
81+
ttl,
82+
updateTime,
83+
value: promise
84+
// value: _resolved ? Promise.resolve(value) : Promise.reject(value)
85+
} as WarmCacheItem<T>,
86+
ttl
87+
);
88+
return promise;
89+
};
90+
91+
try {
92+
const value = await asyncAction();
93+
return handleValue(value, true);
94+
} catch (error) {
95+
return handleValue(error as T, false);
96+
}
97+
}
98+
}

packages/cardano-services/src/Program/programs/providerServer.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { ProviderServerArgs, ProviderServerOptionDescriptions, ServiceNames } fr
3434
import { SrvRecord } from 'dns';
3535
import { TypeormAssetProvider } from '../../Asset/TypeormAssetProvider';
3636
import { TypeormStakePoolProvider } from '../../StakePool/TypeormStakePoolProvider/TypeormStakePoolProvider';
37+
import { WarmCache } from '../../InMemoryCache/WarmCache';
3738
import { createDbSyncMetadataService } from '../../Metadata';
3839
import { createLogger } from 'bunyan';
3940
import { getConnectionConfig, getOgmiosObservableCardanoNode } from '../services';
@@ -99,10 +100,12 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => {
99100
};
100101

101102
const getCache = (ttl: Seconds | 0) => (args.disableDbCache ? new NoCache() : new InMemoryCache(ttl));
103+
const getWarmCache = (ttl: Seconds) => (args.disableDbCache ? new NoCache() : new WarmCache(ttl, Seconds(ttl / 10)));
104+
102105
const getDbCache = () => getCache(args.dbCacheTtl);
103106

104107
// Shared cache across all providers
105-
const healthCheckCache = getCache(args.healthCheckCacheTtl);
108+
const healthCheckCache = getWarmCache(args.healthCheckCacheTtl);
106109

107110
const getEpochMonitor = memoize((dbPool: Pool) => new DbSyncEpochPollService(dbPool, args.epochPollInterval!));
108111

@@ -142,7 +145,13 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => {
142145
const getTypeormStakePoolProvider = withTypeOrmProvider('StakePool', (connectionConfig$) => {
143146
const entities = getEntities(['currentPoolMetrics', 'poolDelisted', 'poolMetadata', 'poolRewards']);
144147

145-
return new TypeormStakePoolProvider(args, { cache: getDbCache(), connectionConfig$, entities, logger });
148+
return new TypeormStakePoolProvider(args, {
149+
cache: getDbCache(),
150+
connectionConfig$,
151+
entities,
152+
healthCheckCache,
153+
logger
154+
});
146155
});
147156

148157
const getNetworkInfoProvider = (cardanoNode: CardanoNode, dbPools: DbPools) => {
@@ -173,7 +182,12 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => {
173182
sharedHandleProvider = await withTypeOrmProvider(
174183
'Handle',
175184
async (connectionConfig$) =>
176-
new TypeOrmHandleProvider({ connectionConfig$, entities: getEntities(['handle', 'handleMetadata']), logger })
185+
new TypeOrmHandleProvider({
186+
connectionConfig$,
187+
entities: getEntities(['handle', 'handleMetadata']),
188+
healthCheckCache,
189+
logger
190+
})
177191
)();
178192

179193
return sharedHandleProvider;
@@ -198,6 +212,7 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => {
198212
{
199213
connectionConfig$,
200214
entities: getEntities(['asset']),
215+
healthCheckCache,
201216
logger,
202217
tokenMetadataService
203218
}
@@ -304,6 +319,7 @@ const serviceMapFactory = (options: ServiceMapFactoryOptions) => {
304319
: new NodeTxSubmitProvider({
305320
cardanoNode: getOgmiosObservableCardanoNode(dnsResolver, logger, args),
306321
handleProvider: args.submitValidateHandles ? await getHandleProvider() : undefined,
322+
healthCheckCache,
307323
logger
308324
});
309325
return new TxSubmitHttpService({ logger, txSubmitProvider });

packages/cardano-services/src/TxSubmit/NodeTxSubmitProvider.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import {
1212
TxSubmissionError,
1313
TxSubmitProvider
1414
} from '@cardano-sdk/core';
15+
import { InMemoryCache } from '../InMemoryCache';
1516
import { Logger } from 'ts-log';
1617
import { WithLogger } from '@cardano-sdk/util';
1718

1819
type ObservableTxSubmitter = Pick<ObservableCardanoNode, 'healthCheck$' | 'submitTx'>;
1920
export type NodeTxSubmitProviderProps = WithLogger & {
2021
handleProvider?: HandleProvider;
2122
cardanoNode: ObservableTxSubmitter;
23+
healthCheckCache: InMemoryCache;
2224
};
2325

2426
const emptyMessage = 'ObservableCardanoNode observable completed without emitting';
@@ -49,19 +51,21 @@ export class NodeTxSubmitProvider implements TxSubmitProvider {
4951
#logger: Logger;
5052
#cardanoNode: ObservableTxSubmitter;
5153
#handleProvider?: HandleProvider;
54+
#healthCheckCache: InMemoryCache;
5255

53-
constructor({ handleProvider, cardanoNode, logger }: NodeTxSubmitProviderProps) {
56+
constructor({ handleProvider, cardanoNode, logger, healthCheckCache }: NodeTxSubmitProviderProps) {
5457
this.#handleProvider = handleProvider;
5558
this.#cardanoNode = cardanoNode;
5659
this.#logger = logger;
60+
this.#healthCheckCache = healthCheckCache;
5761
}
5862

5963
async submitTx({ signedTransaction, context }: SubmitTxArgs): Promise<void> {
6064
await this.#throwIfHandleResolutionConflict(context);
6165
await firstValueFrom(this.#cardanoNode.submitTx(signedTransaction)).catch(toProviderError);
6266
}
6367

64-
async healthCheck(): Promise<HealthCheckResponse> {
68+
async #checkHealth(): Promise<HealthCheckResponse> {
6569
const [cardanoNodeHealth, handleProviderHealth] = await Promise.all([
6670
firstValueFrom(this.#cardanoNode.healthCheck$).catch((error): HealthCheckResponse => {
6771
if (error instanceof EmptyError) {
@@ -79,6 +83,10 @@ export class NodeTxSubmitProvider implements TxSubmitProvider {
7983
};
8084
}
8185

86+
async healthCheck(): Promise<HealthCheckResponse> {
87+
return this.#healthCheckCache.get('ogmios_cardano_node', () => this.#checkHealth());
88+
}
89+
8290
async #throwIfHandleResolutionConflict(context: SubmitTxArgs['context']): Promise<void> {
8391
if (context?.handleResolutions && context.handleResolutions.length > 0) {
8492
if (!this.#handleProvider) {

packages/cardano-services/src/util/TypeormProvider/TypeormProvider.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
import { HealthCheckResponse, Milliseconds, Provider } from '@cardano-sdk/core';
2+
import { InMemoryCache } from '../../InMemoryCache';
23
import { TypeormService, TypeormServiceDependencies } from '../TypeormService';
34
import { skip } from 'rxjs';
45

5-
export type TypeormProviderDependencies = Omit<TypeormServiceDependencies, 'connectionTimeout'>;
6+
export type TypeormProviderDependencies = Omit<TypeormServiceDependencies, 'connectionTimeout'> & {
7+
healthCheckCache: InMemoryCache;
8+
};
69

710
const unhealthy = { ok: false, reason: 'Provider error' };
811

912
export abstract class TypeormProvider extends TypeormService implements Provider {
1013
health: HealthCheckResponse = { ok: false, reason: 'not started' };
14+
healthCheckCache: InMemoryCache;
1115

1216
constructor(name: string, dependencies: TypeormProviderDependencies) {
1317
super(name, { ...dependencies, connectionTimeout: Milliseconds(1000) });
1418
// We skip 1 to omit the initial null value of the subject
1519
this.dataSource$.pipe(skip(1)).subscribe((dataSource) => {
1620
this.health = dataSource ? { ok: true } : unhealthy;
1721
});
22+
23+
this.healthCheckCache = dependencies.healthCheckCache;
1824
}
1925

2026
async healthCheck(): Promise<HealthCheckResponse> {
2127
if (this.state === 'running')
2228
try {
23-
await this.withDataSource((dataSource) => dataSource.query('SELECT 1'));
29+
await this.healthCheckCache.get(`typeorm_db_${this.name}`, () =>
30+
this.withDataSource((dataSource) => dataSource.query('SELECT 1'))
31+
);
2432
} catch {
2533
return unhealthy;
2634
}

packages/cardano-services/test/Asset/TypeOrmNftMetadataService.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Cardano, util } from '@cardano-sdk/core';
2-
import { TypeOrmNftMetadataService, createDnsResolver, getConnectionConfig, getEntities } from '../../src';
2+
import { NoCache, TypeOrmNftMetadataService, createDnsResolver, getConnectionConfig, getEntities } from '../../src';
33
import { logger, mockProviders } from '@cardano-sdk/util-dev';
44

55
describe('TypeOrmNftMetadataService', () => {
@@ -11,7 +11,7 @@ describe('TypeOrmNftMetadataService', () => {
1111
const connectionConfig$ = getConnectionConfig(dnsResolver, 'test', 'Asset', {
1212
postgresConnectionStringAsset: process.env.POSTGRES_CONNECTION_STRING_ASSET!
1313
});
14-
service = new TypeOrmNftMetadataService({ connectionConfig$, entities, logger });
14+
service = new TypeOrmNftMetadataService({ connectionConfig$, entities, healthCheckCache: new NoCache(), logger });
1515
await service.initialize();
1616
await service.start();
1717
});

packages/cardano-services/test/Asset/TypeormAssetProvider/TypeormAssetFixtureBuilder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Asset, Cardano } from '@cardano-sdk/core';
22
import { AssetEntity, NftMetadataEntity } from '@cardano-sdk/projection-typeorm';
33
import { IsNull, Not } from 'typeorm';
44
import { Logger } from 'ts-log';
5+
import { NoCache } from '../../../src';
56
import { TypeormProvider, TypeormProviderDependencies } from '../../../src/util';
67

78
export enum TypeormAssetWith {
@@ -12,7 +13,7 @@ export class TypeormAssetFixtureBuilder extends TypeormProvider {
1213
#logger: Logger;
1314

1415
constructor({ connectionConfig$, entities, logger }: TypeormProviderDependencies) {
15-
super('TypeormAssetFixtureBuilder', { connectionConfig$, entities, logger });
16+
super('TypeormAssetFixtureBuilder', { connectionConfig$, entities, healthCheckCache: new NoCache(), logger });
1617
this.#logger = logger;
1718
}
1819

0 commit comments

Comments
 (0)