From 18a1d4372ceba52abae1ecc697ed9babf1c09743 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Mon, 20 Nov 2023 08:56:45 -0600 Subject: [PATCH 1/2] fix(read): return read interaction errors if they fail Some read interactions are failing, and right now we assume the input is incorrect. This modifies the response/error to include the read interaction error messaging, and throw either an `EvaluationError` or `UnknownError` depending on what the response from `viewState()` returns. --- src/api/warp.ts | 72 ++++++++++++++++++++++++++++++------------ src/middleware/warp.ts | 2 +- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/api/warp.ts b/src/api/warp.ts index f8e92d8..c0fc3e3 100644 --- a/src/api/warp.ts +++ b/src/api/warp.ts @@ -14,19 +14,21 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { EvaluationManifest, EvaluationOptions, Warp } from 'warp-contracts'; +import { + EvalStateResult, + EvaluationManifest, + EvaluationOptions, + InteractionResult, + SortKeyCacheResult, + Warp, +} from 'warp-contracts'; import { DEFAULT_EVALUATION_OPTIONS, EVALUATION_TIMEOUT_MS, allowedContractTypes, } from '../constants'; import { ContractType, EvaluatedContractState } from '../types'; -import { - BadRequestError, - EvaluationError, - NotFoundError, - UnknownError, -} from '../errors'; +import { EvaluationError, NotFoundError, UnknownError } from '../errors'; import * as _ from 'lodash'; import { EvaluationTimeoutError } from '../errors'; import { createHash } from 'crypto'; @@ -37,7 +39,14 @@ import winston from 'winston'; import { ParsedUrlQuery } from 'querystring'; // cache duplicate requests on the same instance within a short period of time -const requestMap: Map | undefined> = new Map(); +const stateRequestMap: Map< + string, + Promise>> +> = new Map(); +const readRequestMap: Map< + string, + Promise> +> = new Map(); // Convenience class for read through caching class ContractStateCacheKey { @@ -154,7 +163,7 @@ async function readThroughToContractState( // Prevent multiple in-flight requests for the same contract state // This could be needed if the read through cache gets overwhelmed - const inFlightRequest = requestMap.get(cacheId); + const inFlightRequest = stateRequestMap.get(cacheId); if (inFlightRequest) { logger?.debug('Deduplicating in flight requests for contract state...', { contractTxId, @@ -180,7 +189,7 @@ async function readThroughToContractState( // set cached value for multiple requests during initial promise const readStatePromise = contract.readState(); - requestMap.set(cacheId, readStatePromise); + stateRequestMap.set(cacheId, readStatePromise); readStatePromise .catch((error: unknown) => { @@ -196,11 +205,20 @@ async function readThroughToContractState( cacheId, }); // remove the cached request whether it completes or fails - requestMap.delete(cacheId); + stateRequestMap.delete(cacheId); }); // await the response - const { cachedValue, sortKey } = await requestMap.get(cacheId); + const stateEvaluationResult = await stateRequestMap.get(cacheId); + if (!stateEvaluationResult) { + logger?.error('Contract state did not return a result!', { + contractTxId, + cacheKey: cacheKey.toString(), + }); + throw new UnknownError(`Unknown error occurred evaluating contract state.`); + } + + const { cachedValue, sortKey } = stateEvaluationResult; logger?.debug('Successfully evaluated contract state.', { contractTxId, cacheKey: cacheKey.toString(), @@ -285,8 +303,9 @@ export async function getContractState({ export async function readThroughToContractReadInteraction( cacheKey: ContractReadInteractionCacheKey, ): Promise<{ - result: any; + result: unknown; evaluationOptions: Partial; + input: unknown; }> { const { contractTxId, evaluationOptions, warp, logger, functionName, input } = cacheKey; @@ -298,7 +317,7 @@ export async function readThroughToContractReadInteraction( // Prevent multiple in-flight requests for the same contract state // This could be needed if the read through cache gets overwhelmed - const inFlightRequest = requestMap.get(cacheId); + const inFlightRequest = readRequestMap.get(cacheId); if (inFlightRequest) { logger?.debug('Deduplicating in flight requests for read interaction...', { contractTxId, @@ -307,6 +326,7 @@ export async function readThroughToContractReadInteraction( const { result } = await inFlightRequest; return { result, + input, evaluationOptions, }; } @@ -326,7 +346,7 @@ export async function readThroughToContractReadInteraction( function: functionName, ...input, }); - requestMap.set(cacheId, readInteractionPromise); + readRequestMap.set(cacheId, readInteractionPromise); readInteractionPromise .catch((error: unknown) => { @@ -342,24 +362,35 @@ export async function readThroughToContractReadInteraction( cacheId, }); // remove the cached request whether it completes or fails - requestMap.delete(cacheId); + readRequestMap.delete(cacheId); }); // await the response - const { result } = await requestMap.get(cacheId); + const readInteractionResult = await readRequestMap.get(cacheId); // we shouldn't return read interactions that don't have a result - if (!result) { + if (!readInteractionResult) { logger?.error('Read interaction did not return a result!', { contractTxId, cacheKey: cacheKey.toString(), input, }); - throw new BadRequestError( - `Invalid read interaction for contract ${contractTxId}.`, + throw new UnknownError( + `Failed to evaluate read interaction for ${contractTxId}.`, ); } + const { result, error, errorMessage } = readInteractionResult; + + if (error) { + logger?.error('Read interaction failed!', { + contractTxId, + cacheKey: cacheKey.toString(), + input, + }); + throw new EvaluationError(errorMessage); + } + logger?.debug('Successfully evaluated read interaction on contract.', { contractTxId, cacheKey: cacheKey.toString(), @@ -367,6 +398,7 @@ export async function readThroughToContractReadInteraction( return { result, + input, evaluationOptions, }; } diff --git a/src/middleware/warp.ts b/src/middleware/warp.ts index c139178..9ff49dd 100644 --- a/src/middleware/warp.ts +++ b/src/middleware/warp.ts @@ -41,7 +41,7 @@ const warp = WarpFactory.forMainnet( ).useStateCache( new LmdbCache(defaultCacheOptions, { maxEntriesPerContract: 1000, - minEntriesPerContract: 10, + minEntriesPerContract: 0, }), ); From c223ee82e943a6e829c334733d3014484a01567e Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Mon, 20 Nov 2023 09:07:18 -0600 Subject: [PATCH 2/2] chore(error): return error message if it exists --- src/api/warp.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/api/warp.ts b/src/api/warp.ts index c0fc3e3..24bc79b 100644 --- a/src/api/warp.ts +++ b/src/api/warp.ts @@ -121,7 +121,8 @@ class ContractReadInteractionCacheKey { const contractReadInteractionCache: ReadThroughPromiseCache< ContractReadInteractionCacheKey, { - result: any; + result: unknown; + input: unknown; evaluationOptions: Partial; } > = new ReadThroughPromiseCache({ @@ -382,11 +383,13 @@ export async function readThroughToContractReadInteraction( const { result, error, errorMessage } = readInteractionResult; - if (error) { + if (error || errorMessage) { logger?.error('Read interaction failed!', { contractTxId, cacheKey: cacheKey.toString(), input, + error, + errorMessage, }); throw new EvaluationError(errorMessage); }