feat: Add persistent cache for the contract's data #225

This commit is contained in:
ppe
2022-09-27 12:05:47 +02:00
committed by just_ppe
parent 0b9d32f865
commit 70b9d3b8cf
9 changed files with 83 additions and 114 deletions

View File

@@ -60,7 +60,10 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => {
}, },
'mainnet' 'mainnet'
) )
.useWarpGateway({ ...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null }) .useWarpGateway({...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null}, {
...defaultCacheOptions,
inMemory: true
})
.build() .build()
.contract(contractTxId) .contract(contractTxId)
.setEvaluationOptions({ .setEvaluationOptions({
@@ -97,7 +100,10 @@ describe.each(chunkedVm)('v1 compare.suite (VM2) %#', (contracts: string[]) => {
}, },
'mainnet' 'mainnet'
) )
.useWarpGateway({ ...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null }) .useWarpGateway({...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null}, {
...defaultCacheOptions,
inMemory: true
})
.build() .build()
.contract(contractTxId) .contract(contractTxId)
.setEvaluationOptions({ .setEvaluationOptions({
@@ -130,7 +136,10 @@ describe.each(chunkedGw)('gateways compare.suite %#', (contracts: string[]) => {
}, },
'mainnet' 'mainnet'
) )
.useWarpGateway({ ...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null }) .useWarpGateway({...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null}, {
...defaultCacheOptions,
inMemory: true
})
.build(); .build();
const result = await warpR const result = await warpR
.contract(contractTxId) .contract(contractTxId)
@@ -182,7 +191,10 @@ describe('readState', () => {
}, },
'mainnet' 'mainnet'
) )
.useWarpGateway({ ...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null }) .useWarpGateway({...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null}, {
...defaultCacheOptions,
inMemory: true
})
.build() .build()
.contract(contractTxId) .contract(contractTxId)
.setEvaluationOptions({ .setEvaluationOptions({
@@ -210,7 +222,10 @@ describe('readState', () => {
}, },
'mainnet' 'mainnet'
) )
.useWarpGateway({ ...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null }) .useWarpGateway({...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null}, {
...defaultCacheOptions,
inMemory: true
})
.build() .build()
.contract(contractTxId) .contract(contractTxId)
.connect(jwk) .connect(jwk)

View File

@@ -22,7 +22,7 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
* and there doesn't seem to be any public interface/abstract type for all Level implementations * and there doesn't seem to be any public interface/abstract type for all Level implementations
* (the AbstractLevel is not exported from the package...) * (the AbstractLevel is not exported from the package...)
*/ */
private db: MemoryLevel; private readonly db: MemoryLevel;
constructor(cacheOptions: CacheOptions) { constructor(cacheOptions: CacheOptions) {
if (cacheOptions.inMemory) { if (cacheOptions.inMemory) {

View File

@@ -13,7 +13,7 @@ import { WarpGatewayInteractionsLoader } from './modules/impl/WarpGatewayInterac
import { InteractionsLoader } from './modules/InteractionsLoader'; import { InteractionsLoader } from './modules/InteractionsLoader';
import { StateEvaluator, EvalStateResult } from './modules/StateEvaluator'; import { StateEvaluator, EvalStateResult } from './modules/StateEvaluator';
import { WarpEnvironment, Warp } from './Warp'; import { WarpEnvironment, Warp } from './Warp';
import { GatewayOptions } from './WarpFactory'; import { CacheOptions, GatewayOptions } from './WarpFactory';
export class WarpBuilder { export class WarpBuilder {
private _definitionLoader?: DefinitionLoader; private _definitionLoader?: DefinitionLoader;
@@ -55,7 +55,7 @@ export class WarpBuilder {
return this.build(); return this.build();
} }
public useWarpGateway(gatewayOptions: GatewayOptions): WarpBuilder { public useWarpGateway(gatewayOptions: GatewayOptions, cacheOptions: CacheOptions): WarpBuilder {
this._interactionsLoader = new CacheableInteractionsLoader( this._interactionsLoader = new CacheableInteractionsLoader(
new WarpGatewayInteractionsLoader( new WarpGatewayInteractionsLoader(
gatewayOptions.address, gatewayOptions.address,
@@ -66,13 +66,13 @@ export class WarpBuilder {
this._definitionLoader = new WarpGatewayContractDefinitionLoader( this._definitionLoader = new WarpGatewayContractDefinitionLoader(
gatewayOptions.address, gatewayOptions.address,
this._arweave, this._arweave,
new MemCache() cacheOptions
); );
return this; return this;
} }
public useArweaveGateway(): WarpBuilder { public useArweaveGateway(): WarpBuilder {
this._definitionLoader = new ContractDefinitionLoader(this._arweave, new MemCache()); this._definitionLoader = new ContractDefinitionLoader(this._arweave);
this._interactionsLoader = new CacheableInteractionsLoader( this._interactionsLoader = new CacheableInteractionsLoader(
new ArweaveGatewayInteractionsLoader(this._arweave, this._environment) new ArweaveGatewayInteractionsLoader(this._arweave, this._environment)
); );

View File

@@ -138,6 +138,6 @@ export class WarpFactory {
cacheOptions: CacheOptions = defaultCacheOptions, cacheOptions: CacheOptions = defaultCacheOptions,
environment: WarpEnvironment environment: WarpEnvironment
): Warp { ): Warp {
return this.custom(arweave, cacheOptions, environment).useWarpGateway(gatewayOptions).build(); return this.custom(arweave, cacheOptions, environment).useWarpGateway(gatewayOptions, cacheOptions).build();
} }
} }

View File

@@ -19,23 +19,14 @@ export class ContractDefinitionLoader implements DefinitionLoader {
protected arweaveWrapper: ArweaveWrapper; protected arweaveWrapper: ArweaveWrapper;
constructor( constructor(private readonly arweave: Arweave) {
private readonly arweave: Arweave,
// TODO: cache should be removed from the core layer and implemented in a wrapper of the core implementation
protected readonly cache?: WarpCache<string, ContractDefinition<unknown>>
) {
this.arweaveWrapper = new ArweaveWrapper(arweave); this.arweaveWrapper = new ArweaveWrapper(arweave);
} }
async load<State>(contractTxId: string, evolvedSrcTxId?: string): Promise<ContractDefinition<State>> { async load<State>(contractTxId: string, evolvedSrcTxId?: string): Promise<ContractDefinition<State>> {
if (!evolvedSrcTxId && this.cache?.contains(contractTxId)) {
this.logger.debug('ContractDefinitionLoader: Hit from cache!');
return Promise.resolve(this.cache?.get(contractTxId) as ContractDefinition<State>);
}
const benchmark = Benchmark.measure(); const benchmark = Benchmark.measure();
const contract = await this.doLoad<State>(contractTxId, evolvedSrcTxId); const contract = await this.doLoad<State>(contractTxId, evolvedSrcTxId);
this.logger.info(`Contract definition loaded in: ${benchmark.elapsed()}`); this.logger.info(`Contract definition loaded in: ${benchmark.elapsed()}`);
this.cache?.put(contractTxId, contract);
return contract; return contract;
} }

View File

@@ -13,6 +13,9 @@ import { stripTrailingSlash } from '../../../utils/utils';
import { DefinitionLoader } from '../DefinitionLoader'; import { DefinitionLoader } from '../DefinitionLoader';
import { WarpCache } from '../../../cache/WarpCache'; import { WarpCache } from '../../../cache/WarpCache';
import { WasmSrc } from './wasm/WasmSrc'; import { WasmSrc } from './wasm/WasmSrc';
import { CacheOptions } from '../../WarpFactory';
import { MemoryLevel } from 'memory-level';
import { Level } from 'level';
/** /**
* An extension to {@link ContractDefinitionLoader} that makes use of * An extension to {@link ContractDefinitionLoader} that makes use of
@@ -26,26 +29,55 @@ export class WarpGatewayContractDefinitionLoader implements DefinitionLoader {
private readonly rLogger = LoggerFactory.INST.create('WarpGatewayContractDefinitionLoader'); private readonly rLogger = LoggerFactory.INST.create('WarpGatewayContractDefinitionLoader');
private contractDefinitionLoader: ContractDefinitionLoader; private contractDefinitionLoader: ContractDefinitionLoader;
private arweaveWrapper: ArweaveWrapper; private arweaveWrapper: ArweaveWrapper;
private readonly db: MemoryLevel<string, any>;
constructor( constructor(
private readonly baseUrl: string, private readonly baseUrl: string,
arweave: Arweave, arweave: Arweave,
private readonly cache?: WarpCache<string, ContractDefinition<unknown>> cacheOptions: CacheOptions
) { ) {
this.baseUrl = stripTrailingSlash(baseUrl); this.baseUrl = stripTrailingSlash(baseUrl);
this.contractDefinitionLoader = new ContractDefinitionLoader(arweave, cache); this.contractDefinitionLoader = new ContractDefinitionLoader(arweave);
this.arweaveWrapper = new ArweaveWrapper(arweave); this.arweaveWrapper = new ArweaveWrapper(arweave);
if (cacheOptions.inMemory) {
this.db = new MemoryLevel<string, any>({ valueEncoding: 'json' });
} else {
if (!cacheOptions.dbLocation) {
throw new Error('LevelDb cache configuration error - no db location specified');
}
const dbLocation = cacheOptions.dbLocation;
this.db = new Level<string, any>(`${dbLocation}/contracts`, { valueEncoding: 'json' });
}
} }
async load<State>(contractTxId: string, evolvedSrcTxId?: string): Promise<ContractDefinition<State>> { async load<State>(contractTxId: string, evolvedSrcTxId?: string): Promise<ContractDefinition<State>> {
if (!evolvedSrcTxId && this.cache?.contains(contractTxId)) { let cacheKey = contractTxId;
if (evolvedSrcTxId) {
cacheKey += `_${evolvedSrcTxId}`;
}
let cacheResult = null;
try {
cacheResult = await this.db.get(cacheKey);
} catch (e: any) {
if (e.code == 'LEVEL_NOT_FOUND') {
cacheResult = null;
} else {
throw e;
}
}
if (cacheResult) {
this.rLogger.debug('WarpGatewayContractDefinitionLoader: Hit from cache!'); this.rLogger.debug('WarpGatewayContractDefinitionLoader: Hit from cache!');
return Promise.resolve(this.cache?.get(contractTxId) as ContractDefinition<State>); if (cacheResult.contractType == 'wasm') {
cacheResult.srcBinary = Buffer.from(cacheResult.srcBinary.data);
}
return cacheResult;
} }
const benchmark = Benchmark.measure(); const benchmark = Benchmark.measure();
const contract = await this.doLoad<State>(contractTxId, evolvedSrcTxId); const contract = await this.doLoad<State>(contractTxId, evolvedSrcTxId);
this.rLogger.info(`Contract definition loaded in: ${benchmark.elapsed()}`); this.rLogger.info(`Contract definition loaded in: ${benchmark.elapsed()}`);
this.cache?.put(contractTxId, contract); await this.db.put(cacheKey, contract);
return contract; return contract;
} }

View File

@@ -31,27 +31,9 @@ async function main() {
logging: false // Enable network request logging logging: false // Enable network request logging
}); });
// Base Contract - e.g. a PST in a separate library const warp = WarpFactory.forMainnet({...defaultCacheOptions, inMemory: false});
const BaseContract = {
handle: function init(state, action) {
//blah blah
}
};
// You own contract, that imports the base contract
(function() {
const baseHandle = BaseContract.handle;
BaseContract.handle = handle;
function handle(state, action) {
baseHandle.call(BaseContract, [state, action]);
// you contract's additional code.
}
})();
const warp = WarpFactory.forMainnet({...defaultCacheOptions, inMemory: true});
try { try {
const contract = warp.contract("6VHC739Bry-PuwE9xxYuPgj6HK75MQKCWyo8yf2fi-k"); const contract = warp.contract("5Yt1IujBmOm1LSux9KDUTjCE7rJqepzP7gZKf_DyzWI");
const cacheResult = await contract const cacheResult = await contract
.setEvaluationOptions({ .setEvaluationOptions({
allowBigInt: true allowBigInt: true

View File

@@ -17,7 +17,7 @@ async function main() {
}); });
try { try {
const warp = WarpFactory.forMainnet({...defaultCacheOptions, inMemory: true}); const warp = WarpFactory.forMainnet({...defaultCacheOptions, inMemory: false});
/*const warp = WarpFactory /*const warp = WarpFactory
.custom(arweave, { .custom(arweave, {
...defaultCacheOptions, ...defaultCacheOptions,
@@ -91,6 +91,8 @@ async function main() {
logger.info("Result", cachedValue.state); logger.info("Result", cachedValue.state);
logger.info("Validity", cachedValue.validity); logger.info("Validity", cachedValue.validity);
const result2 = await contract.readState();
} catch (e) { } catch (e) {
logger.error(e) logger.error(e)

View File

@@ -2,84 +2,31 @@
import {Level} from "level"; import {Level} from "level";
import { MemoryLevel } from 'memory-level'; import { MemoryLevel } from 'memory-level';
import fs from "fs";
import {WasmSrc} from "../src";
import {Buffer} from "buffer";
// Create a database // Create a database
async function test() { async function test() {
//const db = new Level<string, any>('./leveldb', {valueEncoding: 'json'}); //const db = new Level<string, any>('./leveldb', {valueEncoding: 'json'});
const db = new MemoryLevel({ valueEncoding: 'json' }) const db = new MemoryLevel<string, any>({ valueEncoding: 'json' });
const wasmSrc = fs.readFileSync('./tools/data/rust/rust-pst_bg.wasm');
const contractA = db.sublevel<string, any>('n05LTiuWcAYjizXAu-ghegaWjL89anZ6VdvuHcU6dno', {valueEncoding: 'json'}); const contractData = {
const contractB = db.sublevel<string, any>('NwaSMGCdz6Yu5vNjlMtCNBmfEkjYfT-dfYkbQQDGn5s', {valueEncoding: 'json'}); src: wasmSrc,
id: 'n05LTiuWcAYjizXAu-ghegaWjL89anZ6VdvuHcU6dno',
await contractA.put("sort_key_01a", {state: "sort_key_01a"}); srcId: 'foobar'
await contractA.put("sort_key_01b", {state: "sort_key_01b"});
await contractA.put("sort_key_02c", {state: "sort_key_02c"});
await contractA.put("sort_key_03d", {state: "sort_key_03d"});
await contractB.put("sort_key_01e", {state: "sort_key_01e"});
await contractB.put("sort_key_01f", {state: "sort_key_01f"});
await contractB.put("sort_key_02g", {state: "sort_key_02g"});
await contractB.put("sort_key_03h", {state: "sort_key_03h"});
/*for await (const value of contractA.values({lt: '02g'})) {
console.log(value)
} }
console.log("state: " + (await contractB.get('03h')).state); console.log(contractData);
try {
(await contractB.get('06h'));
} catch (e: any) {
console.log(e.code);
}
const keys = await contractB.keys({reverse: true, limit: 1}).all();
console.log(keys.length);
console.log(keys);
db.iterator().seek('zzz'); await db.put("n05LTiuWcAYjizXAu-ghegaWjL89anZ6VdvuHcU6dno", contractData);
const result = await db.iterator().next(); const result = await db.get("n05LTiuWcAYjizXAu-ghegaWjL89anZ6VdvuHcU6dno");
console.log('result last', result); console.log(result);
console.log(Buffer.from(result.src.data));
const entries = await db.iterator({ limit: 10, reverse: true }).all()
console.log('last entries');
for (const [key, value] of entries) {
console.log(key);
}*/
//console.log(contractA.prefix) // '!example!'
const contracts = [];
let lastSortKey = '';
/*for (const key of await db.keys().all()) {
console.log(key);
const sortKey = key.substring(45);
console.log(key.substring(45));
if (sortKey.localeCompare(lastSortKey) > 0) {
lastSortKey = sortKey;
}
}
console.log({lastSortKey});*/
const keys = await db.keys().all();
const result = new Set<string>();
keys.forEach((k) => result.add(k.substring(1, 44)));
console.log(Array.from(result));
// returns sub-levels - i.e. contracts
// console.log(keys.map((k) => k.substring(1, 44)));
} }
test(); test();