Skip to content

Commit 6a6788b

Browse files
authored
fix: use keyv to persist Ethereum metadata cache (#431)
1 parent a861ec5 commit 6a6788b

File tree

10 files changed

+88
-73
lines changed

10 files changed

+88
-73
lines changed

packages/ethereum-storage/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"bn.js": "4.11.8",
5656
"form-data": "2.3.3",
5757
"ipfs-unixfs": "0.1.16",
58+
"keyv": "3.1.0",
5859
"sinon": "7.3.2",
5960
"web3-eth": "1.0.0-beta.37",
6061
"web3-utils": "1.0.0-beta.37"

packages/ethereum-storage/src/lib/ethereum-metadata-cache.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { StorageTypes } from '@requestnetwork/types';
22
import SmartContractManager from './smart-contract-manager';
33

4+
import * as Keyv from 'keyv';
5+
46
/**
57
* Allows to save and retrieve ethereum metadata associated to a data id
68
* Metadata represents general information about the Ethereum network used like network name and smart contract address
@@ -12,7 +14,7 @@ export default class EthereumMetadataCache {
1214
* Store the ethereum metadata for a data id in a dictionary
1315
* This attribute is left public for mocking purpose to facilitate tests on the module
1416
*/
15-
public metadataCache: { [dataId: string]: StorageTypes.IEthereumMetadata } = {};
17+
public metadataCache: Keyv<StorageTypes.IEthereumMetadata>;
1618

1719
/**
1820
* Manager for the storage smart contract
@@ -23,24 +25,30 @@ export default class EthereumMetadataCache {
2325
/**
2426
* Constructor
2527
* @param smartContractManager Instance of SmartContractManager used to get metadata in case they're not registered yet
28+
* @param store a Keyv store to persist the metadata
2629
*/
27-
public constructor(smartContractManager: SmartContractManager) {
30+
public constructor(smartContractManager: SmartContractManager, store?: Keyv.Store<any>) {
2831
this.smartContractManager = smartContractManager;
32+
33+
this.metadataCache = new Keyv<StorageTypes.IEthereumMetadata>({
34+
namespace: 'ethereumMetadata',
35+
store,
36+
});
2937
}
3038

3139
/**
3240
* Saves in the cache the Ethereum metadata related to a dataId
3341
* @param dataId dataId to index ethereum metadata
3442
* @param meta Ethereum metadata related to the dataId
3543
*/
36-
public saveDataIdMeta(dataId: string, meta: StorageTypes.IEthereumMetadata): void {
44+
public async saveDataIdMeta(dataId: string, meta: StorageTypes.IEthereumMetadata): Promise<void> {
3745
// We save the metadata only if it doesn't exist yet
3846
// A user can add the same dataId into the smart contract indefinitely
3947
// Therefore, only the first occurrence of the dataId has valid metadata
4048
// Finding several occurrences of the same dataId is not abnormal and we don't throw an error in this case
4149
// PROT-503: We should ensure the corresponding metadata is the metadata of the first occurrence of the dataId
42-
if (!this.metadataCache[dataId]) {
43-
this.metadataCache[dataId] = meta;
50+
if (!(await this.metadataCache.get(dataId))) {
51+
await this.metadataCache.set(dataId, meta);
4452
}
4553
}
4654

@@ -54,10 +62,12 @@ export default class EthereumMetadataCache {
5462
public async getDataIdMeta(dataId: string): Promise<StorageTypes.IEthereumMetadata> {
5563
// If the metadata has not been saved in the cache yet
5664
// we get them with smartContractManager and save them
57-
if (!this.metadataCache[dataId]) {
58-
this.metadataCache[dataId] = await this.smartContractManager.getMetaFromEthereum(dataId);
65+
let metadata: StorageTypes.IEthereumMetadata | undefined = await this.metadataCache.get(dataId);
66+
if (!metadata) {
67+
metadata = await this.smartContractManager.getMetaFromEthereum(dataId);
68+
await this.metadataCache.set(dataId, metadata);
5969
}
6070

61-
return this.metadataCache[dataId];
71+
return metadata;
6272
}
6373
}

packages/ethereum-storage/src/lib/ethereum-storage.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import EthereumMetadataCache from './ethereum-metadata-cache';
66
import IpfsManager from './ipfs-manager';
77
import SmartContractManager from './smart-contract-manager';
88

9+
import * as Keyv from 'keyv';
10+
911
// rate of the size of the Header of a ipfs file regarding its content size
1012
// used to estimate the size of a ipfs file from the content size
1113
const SAFE_RATE_HEADER_SIZE: number = 0.3;
@@ -51,6 +53,7 @@ export default class EthereumStorage implements StorageTypes.IStorage {
5153
* @param ipfsGatewayConnection Information structure to connect to the ipfs gateway
5254
* @param web3Connection Information structure to connect to the Ethereum network
5355
* @param [options.getLastBlockNumberDelay] the minimum delay to wait between fetches of lastBlockNumber
56+
* @param metadataStore a Keyv store to persist the metadata in ethereumMetadataCache
5457
*/
5558
public constructor(
5659
ipfsGatewayConnection?: StorageTypes.IIpfsGatewayConnection,
@@ -68,6 +71,7 @@ export default class EthereumStorage implements StorageTypes.IStorage {
6871
maxRetries?: number;
6972
retryDelay?: number;
7073
} = {},
74+
metadataStore?: Keyv.Store<any>,
7175
) {
7276
this.maxConcurrency = maxConcurrency || getMaxConcurrency();
7377
this.logger = logger || new Utils.SimpleLogger();
@@ -79,7 +83,7 @@ export default class EthereumStorage implements StorageTypes.IStorage {
7983
maxRetries,
8084
retryDelay,
8185
});
82-
this.ethereumMetadataCache = new EthereumMetadataCache(this.smartContractManager);
86+
this.ethereumMetadataCache = new EthereumMetadataCache(this.smartContractManager, metadataStore);
8387
}
8488

8589
/**
@@ -220,7 +224,7 @@ export default class EthereumStorage implements StorageTypes.IStorage {
220224
}
221225

222226
// Save the metadata of the new dataId into the Ethereum metadata cache
223-
this.ethereumMetadataCache.saveDataIdMeta(dataId, ethereumMetadata);
227+
await this.ethereumMetadataCache.saveDataIdMeta(dataId, ethereumMetadata);
224228

225229
return {
226230
meta: {
@@ -393,7 +397,7 @@ export default class EthereumStorage implements StorageTypes.IStorage {
393397
const ethereumMetadata = contentDataIdAndMeta.meta.metaData[i].ethereum;
394398
if (ethereumMetadata) {
395399
// PROT-504: The saving of dataId's metadata should be encapsulated when retrieving dataId inside smart contract (getPastEvents)
396-
this.ethereumMetadataCache.saveDataIdMeta(dataIds[i], ethereumMetadata);
400+
await this.ethereumMetadataCache.saveDataIdMeta(dataIds[i], ethereumMetadata);
397401
}
398402
}
399403

packages/ethereum-storage/test/lib/ethereum-metadata-cache.ts

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import 'mocha';
22

33
import * as chai from 'chai';
4+
import * as chaiAsPromised from 'chai-as-promised';
45

56
import { StorageTypes } from '@requestnetwork/types';
67
import EthereumMetadataCache from '../../src/lib/ethereum-metadata-cache';
78
import SmartContractManager from '../../src/lib/smart-contract-manager';
89

910
const spies = require('chai-spies');
1011
chai.use(spies);
12+
chai.use(chaiAsPromised);
1113
const expect = chai.expect;
1214
const sandbox = chai.spy.sandbox();
1315

@@ -56,36 +58,38 @@ const getMetaFromEthereumMock = async (hash: string): Promise<StorageTypes.IEthe
5658
};
5759

5860
describe('EthereumMetadataCache', () => {
59-
6061
beforeEach(() => {
6162
sandbox.restore();
6263
smartContractManager = new SmartContractManager();
6364
ethereumMetadataCache = new EthereumMetadataCache(smartContractManager);
6465
});
6566

6667
it('allows to save metadata', async () => {
67-
await expect(ethereumMetadataCache.metadataCache[hashExample1]).to.be.undefined;
68-
await expect(ethereumMetadataCache.metadataCache[hashExample2]).to.be.undefined;
69-
await expect(ethereumMetadataCache.metadataCache[hashExample3]).to.be.undefined;
68+
await expect(ethereumMetadataCache.metadataCache.get(hashExample1)).to.eventually.be.undefined;
69+
await expect(ethereumMetadataCache.metadataCache.get(hashExample2)).to.eventually.be.undefined;
70+
await expect(ethereumMetadataCache.metadataCache.get(hashExample3)).to.eventually.be.undefined;
7071

71-
ethereumMetadataCache.saveDataIdMeta(hashExample1, ethereumMetadataExample1);
72-
ethereumMetadataCache.saveDataIdMeta(hashExample2, ethereumMetadataExample2);
73-
ethereumMetadataCache.saveDataIdMeta(hashExample3, ethereumMetadataExample3);
72+
await ethereumMetadataCache.saveDataIdMeta(hashExample1, ethereumMetadataExample1);
73+
await ethereumMetadataCache.saveDataIdMeta(hashExample2, ethereumMetadataExample2);
74+
await ethereumMetadataCache.saveDataIdMeta(hashExample3, ethereumMetadataExample3);
7475

75-
await expect(ethereumMetadataCache.metadataCache[hashExample1]).to.deep.equal(ethereumMetadataExample1);
76-
await expect(ethereumMetadataCache.metadataCache[hashExample2]).to.deep.equal(ethereumMetadataExample2);
77-
await expect(ethereumMetadataCache.metadataCache[hashExample3]).to.deep.equal(ethereumMetadataExample3);
76+
await expect(ethereumMetadataCache.metadataCache.get(hashExample1)).to.eventually.deep.equal(
77+
ethereumMetadataExample1,
78+
);
79+
await expect(ethereumMetadataCache.metadataCache.get(hashExample2)).to.eventually.deep.equal(
80+
ethereumMetadataExample2,
81+
);
82+
await expect(ethereumMetadataCache.metadataCache.get(hashExample3)).to.eventually.deep.equal(
83+
ethereumMetadataExample3,
84+
);
7885
});
7986

8087
it('allows to retrieve saved metadata', async () => {
81-
const spy = sandbox.on(
82-
smartContractManager,
83-
'getMetaFromEthereum',
84-
);
88+
const spy = sandbox.on(smartContractManager, 'getMetaFromEthereum');
8589

86-
ethereumMetadataCache.saveDataIdMeta(hashExample1, ethereumMetadataExample1);
87-
ethereumMetadataCache.saveDataIdMeta(hashExample2, ethereumMetadataExample2);
88-
ethereumMetadataCache.saveDataIdMeta(hashExample3, ethereumMetadataExample3);
90+
await ethereumMetadataCache.saveDataIdMeta(hashExample1, ethereumMetadataExample1);
91+
await ethereumMetadataCache.saveDataIdMeta(hashExample2, ethereumMetadataExample2);
92+
await ethereumMetadataCache.saveDataIdMeta(hashExample3, ethereumMetadataExample3);
8993

9094
const readReturn1 = await ethereumMetadataCache.getDataIdMeta(hashExample1);
9195
const readReturn2 = await ethereumMetadataCache.getDataIdMeta(hashExample2);
@@ -101,35 +105,34 @@ describe('EthereumMetadataCache', () => {
101105
it('allows to save when trying to read new metadata', async () => {
102106
smartContractManager.getMetaFromEthereum = getMetaFromEthereumMock;
103107

104-
const spy = sandbox.on(
105-
smartContractManager,
106-
'getMetaFromEthereum',
107-
);
108+
const spy = sandbox.on(smartContractManager, 'getMetaFromEthereum');
108109

109-
await expect(ethereumMetadataCache.metadataCache[hashExample1]).to.be.undefined;
110-
await expect(ethereumMetadataCache.metadataCache[hashExample2]).to.be.undefined;
111-
await expect(ethereumMetadataCache.metadataCache[hashExample3]).to.be.undefined;
110+
await expect(ethereumMetadataCache.metadataCache.get(hashExample1)).to.eventually.be.undefined;
111+
await expect(ethereumMetadataCache.metadataCache.get(hashExample2)).to.eventually.be.undefined;
112+
await expect(ethereumMetadataCache.metadataCache.get(hashExample3)).to.eventually.be.undefined;
112113

113114
const readReturn1 = await ethereumMetadataCache.getDataIdMeta(hashExample1);
114115
await expect(readReturn1).to.deep.equal(ethereumMetadataExample1);
115116
await expect(spy).to.have.been.called.once;
116-
await expect(ethereumMetadataCache.metadataCache[hashExample2]).to.be.undefined;
117-
await expect(ethereumMetadataCache.metadataCache[hashExample3]).to.be.undefined;
117+
await expect(ethereumMetadataCache.metadataCache.get(hashExample2)).to.eventually.be.undefined;
118+
await expect(ethereumMetadataCache.metadataCache.get(hashExample3)).to.eventually.be.undefined;
118119

119120
const readReturn2 = await ethereumMetadataCache.getDataIdMeta(hashExample2);
120121
await expect(readReturn2).to.deep.equal(ethereumMetadataExample2);
121122
await expect(spy).to.have.been.called.twice;
122-
await expect(ethereumMetadataCache.metadataCache[hashExample3]).to.be.undefined;
123+
await expect(ethereumMetadataCache.metadataCache.get(hashExample3)).to.eventually.be.undefined;
123124

124125
const readReturn3 = await ethereumMetadataCache.getDataIdMeta(hashExample3);
125126
await expect(readReturn3).to.deep.equal(ethereumMetadataExample3);
126127
await expect(spy).to.have.been.called.exactly(3);
127128
});
128129

129130
it('cannot erase metadata of dataId with new metadata', async () => {
130-
ethereumMetadataCache.saveDataIdMeta(hashExample1, ethereumMetadataExample1);
131-
ethereumMetadataCache.saveDataIdMeta(hashExample1, ethereumMetadataExample2);
131+
await ethereumMetadataCache.saveDataIdMeta(hashExample1, ethereumMetadataExample1);
132+
await ethereumMetadataCache.saveDataIdMeta(hashExample1, ethereumMetadataExample2);
132133

133-
await expect(ethereumMetadataCache.metadataCache[hashExample1]).to.deep.equal(ethereumMetadataExample1);
134+
await expect(ethereumMetadataCache.metadataCache.get(hashExample1)).to.eventually.deep.equal(
135+
ethereumMetadataExample1,
136+
);
134137
});
135138
});

packages/ethereum-storage/test/lib/ethereum-storage.test.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -292,17 +292,14 @@ describe('EthereumStorage', () => {
292292
}
293293
});
294294
it(`allows to save dataId's Ethereum metadata into the metadata cache when append is called`, async () => {
295-
await assert.isUndefined(ethereumStorage.ethereumMetadataCache.metadataCache[hash1]);
295+
await expect(ethereumStorage.ethereumMetadataCache.metadataCache.get(hash1)).to.eventually.be.undefined;
296296

297297
const result = await ethereumStorage.append(content1);
298-
await assert.deepEqual(
299-
result.meta.ethereum,
300-
ethereumStorage.ethereumMetadataCache.metadataCache[hash1],
301-
);
298+
await expect(ethereumStorage.ethereumMetadataCache.metadataCache.get(hash1)).to.eventually.deep.equal(result.meta.ethereum);
302299
});
303300

304301
it(`prevents already saved dataId's Ethereum metadata to be erased in the metadata cache when append is called`, async () => {
305-
await assert.isUndefined(ethereumStorage.ethereumMetadataCache.metadataCache[hash1]);
302+
await expect(ethereumStorage.ethereumMetadataCache.metadataCache.get(hash1)).to.eventually.be.undefined;
306303

307304
const result1 = await ethereumStorage.append(content1);
308305

@@ -327,17 +324,13 @@ describe('EthereumStorage', () => {
327324
const result2 = await ethereumStorage.append(content1);
328325

329326
await assert.notDeepEqual(result1, result2);
330-
331-
await assert.deepEqual(
332-
result1.meta.ethereum,
333-
ethereumStorage.ethereumMetadataCache.metadataCache[hash1],
334-
);
327+
await expect(ethereumStorage.ethereumMetadataCache.metadataCache.get(hash1)).to.eventually.deep.equal(result1.meta.ethereum);
335328
});
336329

337330
it('allows to read a file', async () => {
338331
// For this test, we don't want to use the ethereum metadata cache
339332
// We want to force the retrieval of metadata with getPastEvents function
340-
ethereumStorage.ethereumMetadataCache.saveDataIdMeta = (_dataId, _meta) => {};
333+
ethereumStorage.ethereumMetadataCache.saveDataIdMeta = async (_dataId, _meta) => { };
341334

342335
await ethereumStorage.append(content1);
343336
const result = await ethereumStorage.read(hash1);
@@ -374,7 +367,7 @@ describe('EthereumStorage', () => {
374367
});
375368

376369
it('cannot read if ethereumMetadataCache.getDataIdMeta fail', async () => {
377-
ethereumStorage.ethereumMetadataCache.getDataIdMeta = () => {
370+
ethereumStorage.ethereumMetadataCache.getDataIdMeta = async () => {
378371
throw Error('expected error');
379372
};
380373
await expect(ethereumStorage.read(content1)).to.eventually.rejectedWith(
@@ -450,7 +443,7 @@ describe('EthereumStorage', () => {
450443
it('allows to retrieve all data', async () => {
451444
// For this test, we don't want to use the ethereum metadata cache
452445
// We want to force the retrieval of metadata with getPastEvents function
453-
ethereumStorage.ethereumMetadataCache.saveDataIdMeta = (_dataId, _meta) => {};
446+
ethereumStorage.ethereumMetadataCache.saveDataIdMeta = async (_dataId, _meta) => { };
454447

455448
// These contents have to be appended in order to check their size
456449
await ethereumStorage.append(content1);
@@ -517,7 +510,7 @@ describe('EthereumStorage', () => {
517510
it('doest get meta data if the fees are too low', async () => {
518511
// For this test, we don't want to use the ethereum metadata cache
519512
// We want to force the retrieval of metadata with getPastEvents function
520-
ethereumStorage.ethereumMetadataCache.saveDataIdMeta = (_dataId, _meta) => {};
513+
ethereumStorage.ethereumMetadataCache.saveDataIdMeta = async (_dataId, _meta) => {};
521514
ethereumStorage.smartContractManager.getHashesAndSizesFromEthereum = async (): Promise<
522515
any[]
523516
> => {
@@ -611,7 +604,7 @@ describe('EthereumStorage', () => {
611604
});
612605

613606
it('allows to read a file', async () => {
614-
ethereumStorage.ethereumMetadataCache.saveDataIdMeta = (_dataId, _meta) => {};
607+
ethereumStorage.ethereumMetadataCache.saveDataIdMeta = async (_dataId, _meta) => { };
615608

616609
const content = [content1, content2];
617610
const realSizes = [realSize1, realSize2];

packages/request-node/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,8 @@ Default values correspond to the basic configuration used to run a server in a t
227227
- `--storageConcurrency` Maximum number of concurrent calls to Ethereum or IPFS
228228
- Default value: `'200'`
229229
- Environment variable name: `$STORAGE_MAX_CONCURRENCY`
230-
- `--transactionIndexFilePath` Path to a file to persist the transaction index for faster intialization
231-
- Environment variable name: `$TRANSACTION_INDEX_FILE_PATH`
230+
- `--initializationStorageFilePath` Path to a file to persist the ethereum metadata and transaction index for faster initialization
231+
- Environment variable name: `$INITIALIZATION_STORAGE_FILE_PATH`
232232
- `--logLevel` The maximum level of messages we will log
233233
- Environment variable name: `$LOG_LEVEL`
234234
- Available levels: ERROR, WARN, INFO and DEBUG

packages/request-node/src/config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,12 @@ export function getEthereumRetryDelay(): number {
209209
}
210210

211211
/**
212-
* Get the transaction index storage (a json-like file) path.
213-
* @returns the path to the json-like file that stores the transaction index.
212+
* Get the initialization storage (a json-like file) path.
213+
* @returns the path to the json-like file that stores the initialization data (ethereum metadata and transaction index).
214214
*/
215-
export function getTransactionIndexFilePath(): string | null {
215+
export function getInitializationStorageFilePath(): string | null {
216216
return (
217-
(argv.transactionIndexFilePath as string) || process.env.TRANSACTION_INDEX_FILE_PATH || null
217+
(argv.initializationStorageFilePath as string) || process.env.INITIALIZATION_STORAGE_FILE_PATH || null
218218
);
219219
}
220220

packages/request-node/src/requestNode.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as httpStatus from 'http-status-codes';
77
import KeyvFile from 'keyv-file';
88

99
import Utils from '@requestnetwork/utils';
10-
import { getCustomHeaders, getMnemonic, getTransactionIndexFilePath } from './config';
10+
import { getCustomHeaders, getInitializationStorageFilePath, getMnemonic } from './config';
1111
import getChannelsByTopic from './request/getChannelsByTopic';
1212
import getTransactionsByChannelId from './request/getTransactionsByChannelId';
1313
import persistTransaction from './request/persistTransaction';
@@ -43,17 +43,17 @@ class RequestNode {
4343

4444
this.logger = logger || new Utils.SimpleLogger();
4545

46-
// Use ethereum storage for the storage layer
47-
const ethereumStorage: StorageTypes.IStorage = getEthereumStorage(getMnemonic(), this.logger);
48-
49-
const transactionIndexStoragePath = getTransactionIndexFilePath();
46+
const initializationStoragePath = getInitializationStorageFilePath();
5047

51-
const store = transactionIndexStoragePath
48+
const store = initializationStoragePath
5249
? new KeyvFile({
53-
filename: transactionIndexStoragePath,
50+
filename: initializationStoragePath,
5451
})
5552
: undefined;
5653

54+
// Use ethereum storage for the storage layer
55+
const ethereumStorage: StorageTypes.IStorage = getEthereumStorage(getMnemonic(), this.logger, store);
56+
5757
// Use an in-file Transaction index if a path is specified, an in-memory otherwise
5858
const transactionIndex = new TransactionIndex(store);
5959

packages/request-node/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const startNode = async (): Promise<void> => {
2020
IPFS protocol: ${config.getIpfsProtocol()}
2121
IPFS timeout: ${config.getIpfsTimeout()}
2222
Storage concurrency: ${config.getStorageConcurrency()}
23-
Transaction Index path: ${config.getTransactionIndexFilePath()}
23+
Initialization storage path: ${config.getInitializationStorageFilePath()}
2424
`;
2525

2626
logger.info(serverMessage);

0 commit comments

Comments
 (0)