Skip to content

Commit db1e349

Browse files
codybornclaudealanhwu
authored
fix: cut Get Orders read amplification on hot GSI partitions (#696)
* fix: cut Get Orders read amplification on hot GSI partitions GET /dutch-auction/orders is throwing "Throughput exceeds the current capacity of your table or index" in prod. The Orders table is PAY_PER_REQUEST, so there is no table-level ceiling to hit -- the limit being reached is the ~3000 RCU/s DynamoDB caps a single partition at. Filler polling concentrates on one GSI partition key (`1_open` on chainId_orderStatus-createdAt-all), and two multipliers sit on top of it. Bound the retry loop: MAX_QUERY_RETRY 10 -> 3. `orderType` is applied as a post-query filter rather than a key condition, so each retry is another full 50-item page read against the same partition, billed in full even when it matches no rows. Worst case per request drops from 11 reads to 4. Add a 250ms cache over the list-query path. Fillers poll the same chainId/orderStatus shapes continuously, so repeats inside that window are byte-identical; collapsing them cuts reads on the hot key by roughly TTL/handler-latency. The key covers every input that can change the result set, and callers get a copied array so a cached entry cannot be mutated downstream. Hit/miss metrics are emitted so the real rate is visible before deciding whether a shared cache is also needed. The cache is opt-in per repository, not a repository default: the unimind cron writes orders and re-reads them expecting fresh data, so only the read-only get-orders path passes it in. TTL is configurable via GET_ORDERS_CACHE_TTL_MS, and 0 disables it without a code change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address review — cache entry expiry, kill switch, retry visibility Stamp cache entries from when the query finished rather than when it started. A throttled partition retries with backoff, so a query can outlast the 250ms TTL; dating the entry from the query start wrote it already expired and silently disabled the cache under exactly the load it exists to absorb. Covered by a regression test that fails against the previous behaviour. Plumb GET_ORDERS_CACHE_TTL_MS through the beta and prod stage envVars. CDK writes the complete Environment.Variables map on every deploy, so a console-set variable it does not know about is dropped by the next one — the documented rollback path did not survive a deploy. Emit GetOrdersRetryExhausted when a type-filtered query gives up with a cursor still outstanding, so the cost of the lower MAX_QUERY_RETRY is measurable rather than assumed. Correct the copyQueryResult comment: the copy is shallow, so it protects the array but not the order objects, which must be treated as read-only. Build the query-cache tests on a locally constructed QueryCache instead of the exported singleton, whose TTL comes from the environment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: address review round 2 — dedupe multi-status merges, per-endpoint caches, drop retry cut - Dedupe getOrdersForMultipleStatuses by orderHash: per-status sub-queries are independent cache entries, so one can serve a stale page listing the same order under a second status; keep the non-OPEN copy since transitions flow away from OPEN - Opt the read-only GET /limit-orders lambda into the query cache — it polls the same hot single-status GSI partitions on the LimitOrders table - Move cache construction out of the generic repository into per-endpoint modules; hit/miss metrics are now named by the cache instance (GetOrdersQueryCache*, GetLimitOrdersQueryCache*) instead of hardcoded in the shared base class - Gate cache bookkeeping on enabled so the GET_ORDERS_CACHE_TTL_MS=0 kill switch also silences the miss metric (a disabled cache no longer looks broken), and skip key building entirely on uncached repositories - Revert MAX_QUERY_RETRY to 10 and drop the GetOrdersRetryExhausted metric: the cache absorbs the hot-partition read amplification, and the deeper retry budget keeps type-filtered pages full for cursor-ignoring pollers - Extract the five duplicated per-type retry loops into fetchOrderPages - Document GET_ORDERS_CACHE_TTL_MS in CLAUDE.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: fix comments --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Alan Wu <60207036+alanhwu@users.noreply.github.com> Co-authored-by: Alan Wu <alanwu100@gmail.com>
1 parent b2a20e0 commit db1e349

17 files changed

Lines changed: 592 additions & 130 deletions

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ Required for deployment:
4646
- `RPC_HEADER_SECRET` - Value sent as the `x-internal-service-secret` header on all RPC requests (see `RPC_HEADERS` in `lib/util/constants.ts`). Omitted when unset.
4747
- `FAILED_EVENT_DESTINATION_ARN` - Failed event SNS ARN
4848

49+
Optional:
50+
- `GET_ORDERS_CACHE_TTL_MS` - TTL for the read-path query cache on the get-orders/get-limit-orders Lambdas (default 250; set to `0` to disable the cache).
51+
4952
For tests:
5053
- `UNISWAP_API` - Deployed API URL (e2e tests)
5154
- `LABS_COSIGNER` - Valid EVM address (unit tests)

bin/app.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,18 @@ export class APIPipeline extends Stack {
200200
.secretValueFromJson('ACTIVE_ORDER_EVENT_DESTINATION_ARN_BETA')
201201
.toString(),
202202
POSTED_ORDER_DESTINATION_ARN: resourceArnSecret.secretValueFromJson('POSTED_ORDER_DESTINATION_BETA').toString(),
203-
UNIMIND_RESPONSE_DESTINATION_ARN: resourceArnSecret.secretValueFromJson('UNIMIND_RESPONSE_DESTINATION_ARN_BETA').toString(),
204-
UNIMIND_PARAMETER_UPDATE_DESTINATION_ARN: resourceArnSecret.secretValueFromJson('UNIMIND_PARAMETER_UPDATE_DESTINATION_ARN_BETA').toString(),
205-
CLOUDWATCH_LOGS_FIREHOSE_ROLE_ARN: resourceArnSecret.secretValueFromJson('CLOUDWATCH_LOGS_FIREHOSE_ROLE_ARN_BETA').toString(),
203+
UNIMIND_RESPONSE_DESTINATION_ARN: resourceArnSecret
204+
.secretValueFromJson('UNIMIND_RESPONSE_DESTINATION_ARN_BETA')
205+
.toString(),
206+
UNIMIND_PARAMETER_UPDATE_DESTINATION_ARN: resourceArnSecret
207+
.secretValueFromJson('UNIMIND_PARAMETER_UPDATE_DESTINATION_ARN_BETA')
208+
.toString(),
209+
CLOUDWATCH_LOGS_FIREHOSE_ROLE_ARN: resourceArnSecret
210+
.secretValueFromJson('CLOUDWATCH_LOGS_FIREHOSE_ROLE_ARN_BETA')
211+
.toString(),
206212
THROTTLE_PER_FIVE_MINS: '3000',
213+
// Get Orders query cache TTL. Set to '0' and deploy to disable the cache.
214+
GET_ORDERS_CACHE_TTL_MS: '250',
207215
REGION: 'us-east-2', //needed in checkOrderStatusHandler to kick off step function retries
208216
LABS_COSIGNER: labsCosignerBeta.secretValue.toString(),
209217
LABS_PRIORITY_COSIGNER: labsPriorityCosignerBeta.secretValue.toString(),
@@ -237,10 +245,18 @@ export class APIPipeline extends Stack {
237245
.secretValueFromJson('ACTIVE_ORDER_EVENT_DESTINATION_ARN_PROD')
238246
.toString(),
239247
POSTED_ORDER_DESTINATION_ARN: resourceArnSecret.secretValueFromJson('POSTED_ORDER_DESTINATION_PROD').toString(),
240-
UNIMIND_RESPONSE_DESTINATION_ARN: resourceArnSecret.secretValueFromJson('UNIMIND_RESPONSE_DESTINATION_ARN_PROD').toString(),
241-
UNIMIND_PARAMETER_UPDATE_DESTINATION_ARN: resourceArnSecret.secretValueFromJson('UNIMIND_PARAMETER_UPDATE_DESTINATION_ARN_PROD').toString(),
242-
CLOUDWATCH_LOGS_FIREHOSE_ROLE_ARN: resourceArnSecret.secretValueFromJson('CLOUDWATCH_LOGS_FIREHOSE_ROLE_ARN_PROD').toString(),
248+
UNIMIND_RESPONSE_DESTINATION_ARN: resourceArnSecret
249+
.secretValueFromJson('UNIMIND_RESPONSE_DESTINATION_ARN_PROD')
250+
.toString(),
251+
UNIMIND_PARAMETER_UPDATE_DESTINATION_ARN: resourceArnSecret
252+
.secretValueFromJson('UNIMIND_PARAMETER_UPDATE_DESTINATION_ARN_PROD')
253+
.toString(),
254+
CLOUDWATCH_LOGS_FIREHOSE_ROLE_ARN: resourceArnSecret
255+
.secretValueFromJson('CLOUDWATCH_LOGS_FIREHOSE_ROLE_ARN_PROD')
256+
.toString(),
243257
THROTTLE_PER_FIVE_MINS: '3000',
258+
// Get Orders query cache TTL. Set to '0' and deploy to disable the cache.
259+
GET_ORDERS_CACHE_TTL_MS: '250',
244260
REGION: 'us-east-2', //needed in checkOrderStatusHandler to kick off step function retries
245261
LABS_COSIGNER: labsCosignerProd.secretValue.toString(),
246262
LABS_PRIORITY_COSIGNER: labsPriorityCosignerProd.secretValue.toString(),
@@ -385,6 +401,7 @@ envVars['UNIMIND_PARAMETER_UPDATE_DESTINATION_ARN'] = process.env['UNIMIND_PARAM
385401
envVars['CLOUDWATCH_LOGS_FIREHOSE_ROLE_ARN'] = process.env['CLOUDWATCH_LOGS_FIREHOSE_ROLE_ARN'] || ''
386402
envVars['LABS_COSIGNER'] = process.env['LABS_COSIGNER'] || ''
387403
envVars['LABS_PRIORITY_COSIGNER'] = process.env['LABS_PRIORITY_COSIGNER'] || ''
404+
envVars['GET_ORDERS_CACHE_TTL_MS'] = process.env['GET_ORDERS_CACHE_TTL_MS'] || ''
388405

389406
new APIStack(app, `${SERVICE_NAME}Stack`, {
390407
provisionedConcurrency: process.env.PROVISION_CONCURRENCY ? parseInt(process.env.PROVISION_CONCURRENCY) : 0,

lib/handlers/get-limit-orders/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ONE_DAY_IN_SECONDS } from '../../util/constants'
88

99
import { log } from '../../Logging'
1010
import { LimitOrdersRepository } from '../../repositories/limit-orders-repository'
11+
import { DynamoQuoteMetadataRepository } from '../../repositories/quote-metadata-repository'
1112
import { OrderDispatcher } from '../../services/OrderDispatcher'
1213
import { OffChainRelayOrderValidator } from '../../util/OffChainRelayOrderValidator'
1314
import { OffChainUniswapXOrderValidator } from '../../util/OffChainUniswapXOrderValidator'
@@ -18,9 +19,11 @@ import { GetOrdersHandler } from '../get-orders/handler'
1819
import { OnChainValidatorMap } from '../OnChainValidatorMap'
1920
import { getMaxOpenOrders } from '../post-order/injector'
2021
import { GetLimitOrdersInjector } from './injector'
21-
import { DynamoQuoteMetadataRepository } from '../../repositories/quote-metadata-repository'
22+
import { getLimitOrdersQueryCache } from './query-cache'
2223

23-
const repo = LimitOrdersRepository.create(new DynamoDB.DocumentClient())
24+
// This Lambda only reads, so its repositories share the sub-second query cache. Write
25+
// paths build them without it -- see query-cache.ts.
26+
const repo = LimitOrdersRepository.create(new DynamoDB.DocumentClient(), getLimitOrdersQueryCache)
2427
const quoteMetadataRepository = DynamoQuoteMetadataRepository.create(new DynamoDB.DocumentClient())
2528
const orderValidator = new OffChainUniswapXOrderValidator(() => new Date().getTime() / 1000, ONE_DAY_IN_SECONDS)
2629
const onChainValidatorMap = new OnChainValidatorMap<OrderValidator>()
@@ -44,7 +47,7 @@ const relayOrderService = new RelayOrderService(
4447
relayOrderValidator,
4548
relayOrderValidatorMap,
4649
EventWatcherMap.createRelayEventWatcherMap(),
47-
RelayOrderRepository.create(new DynamoDB.DocumentClient()),
50+
RelayOrderRepository.create(new DynamoDB.DocumentClient(), getLimitOrdersQueryCache),
4851
log,
4952
getMaxOpenOrders,
5053
new FillEventLogger(FILL_EVENT_LOOKBACK_BLOCKS_ON, AnalyticsService.create())

lib/handlers/get-limit-orders/injector.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { LimitOrdersRepository } from '../../repositories/limit-orders-repositor
66
import { ApiInjector, ApiRInj } from '../base/index'
77
import { GetOrdersQueryParams, RawGetOrdersQueryParams } from '../get-orders/schema'
88
import { ContainerInjected, getSharedRequestInjected } from '../shared/get'
9+
import { getLimitOrdersQueryCache } from './query-cache'
910

1011
export interface RequestInjected extends ApiRInj {
1112
limit: number
@@ -21,7 +22,7 @@ export class GetLimitOrdersInjector extends ApiInjector<
2122
> {
2223
public async buildContainerInjected(): Promise<ContainerInjected> {
2324
return {
24-
dbInterface: LimitOrdersRepository.create(new DynamoDB.DocumentClient()),
25+
dbInterface: LimitOrdersRepository.create(new DynamoDB.DocumentClient(), getLimitOrdersQueryCache),
2526
}
2627
}
2728

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { OrdersQueryCache, QueryCache, queryCacheTtlFromEnv } from '../../repositories/QueryCache'
2+
3+
// GET /limit-orders is polled by fillers the same way GET /orders is, against the same
4+
// kind of hot single-status GSI partition on the LimitOrders table. Its own instance
5+
// (rather than sharing get-orders') keeps hit/miss metrics attributable per endpoint.
6+
export const getLimitOrdersQueryCache: OrdersQueryCache = new QueryCache(
7+
queryCacheTtlFromEnv(),
8+
'GetLimitOrdersQueryCache'
9+
)

lib/handlers/get-orders/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@ import { ONE_DAY_IN_SECONDS } from '../../util/constants'
1313

1414
import { log } from '../../Logging'
1515
import { LimitOrdersRepository } from '../../repositories/limit-orders-repository'
16+
import { DynamoQuoteMetadataRepository } from '../../repositories/quote-metadata-repository'
1617
import { OffChainRelayOrderValidator } from '../../util/OffChainRelayOrderValidator'
1718
import { OffChainUniswapXOrderValidator } from '../../util/OffChainUniswapXOrderValidator'
1819
import { FillEventLogger } from '../check-order-status/fill-event-logger'
1920
import { FILL_EVENT_LOOKBACK_BLOCKS_ON } from '../check-order-status/util'
2021
import { EventWatcherMap } from '../EventWatcherMap'
2122
import { OnChainValidatorMap } from '../OnChainValidatorMap'
2223
import { getMaxOpenOrders } from '../post-order/injector'
23-
import { DynamoQuoteMetadataRepository } from '../../repositories/quote-metadata-repository'
24+
import { getOrdersQueryCache } from './query-cache'
2425

25-
const repo = DutchOrdersRepository.create(new DynamoDB.DocumentClient())
26-
const limitRepo = LimitOrdersRepository.create(new DynamoDB.DocumentClient())
26+
// This Lambda only reads, so its repositories share the sub-second query cache. Write
27+
// paths build them without it -- see query-cache.ts.
28+
const repo = DutchOrdersRepository.create(new DynamoDB.DocumentClient(), getOrdersQueryCache)
29+
const limitRepo = LimitOrdersRepository.create(new DynamoDB.DocumentClient(), getOrdersQueryCache)
2730
const quoteMetadataRepository = DynamoQuoteMetadataRepository.create(new DynamoDB.DocumentClient())
2831
const orderValidator = new OffChainUniswapXOrderValidator(() => new Date().getTime() / 1000, ONE_DAY_IN_SECONDS)
2932
const onChainValidatorMap = new OnChainValidatorMap<OrderValidator>()
@@ -48,7 +51,7 @@ const relayOrderService = new RelayOrderService(
4851
relayOrderValidator,
4952
relayOrderValidatorMap,
5053
EventWatcherMap.createRelayEventWatcherMap(),
51-
RelayOrderRepository.create(new DynamoDB.DocumentClient()),
54+
RelayOrderRepository.create(new DynamoDB.DocumentClient(), getOrdersQueryCache),
5255
log,
5356
getMaxOpenOrders,
5457
new FillEventLogger(FILL_EVENT_LOOKBACK_BLOCKS_ON, AnalyticsService.create())

lib/handlers/get-orders/injector.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { BaseOrdersRepository } from '../../repositories/base'
77
import { DutchOrdersRepository } from '../../repositories/dutch-orders-repository'
88
import { ApiInjector, ApiRInj } from '../base/index'
99
import { getSharedRequestInjected } from '../shared/get'
10+
import { getOrdersQueryCache } from './query-cache'
1011
import { GetOrdersQueryParams, RawGetOrdersQueryParams } from './schema'
1112
import { GetOrderTypeQueryParamEnum } from './schema/GetOrderTypeQueryParamEnum'
1213

@@ -25,7 +26,7 @@ export interface ContainerInjected {
2526
export class GetOrdersInjector extends ApiInjector<ContainerInjected, RequestInjected, void, RawGetOrdersQueryParams> {
2627
public async buildContainerInjected(): Promise<ContainerInjected> {
2728
return {
28-
dbInterface: DutchOrdersRepository.create(new DynamoDB.DocumentClient()),
29+
dbInterface: DutchOrdersRepository.create(new DynamoDB.DocumentClient(), getOrdersQueryCache),
2930
}
3031
}
3132

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { OrdersQueryCache, QueryCache, queryCacheTtlFromEnv } from '../../repositories/QueryCache'
2+
3+
// Sub-second cache over the list-query path. Fillers poll the same query shapes
4+
// continuously, so repeats inside this window are collapsed into a single read against
5+
// the hot GSI partition.
6+
//
7+
// Opt-in, not a repository default: background jobs (the unimind cron, the reaper) write
8+
// orders and immediately re-read them expecting fresh data. Only this read-only endpoint
9+
// passes it in. Shared across the repositories this endpoint builds so they pool hits --
10+
// the table name is part of every key, so Orders/LimitOrders/RelayOrders never collide.
11+
export const getOrdersQueryCache: OrdersQueryCache = new QueryCache(queryCacheTtlFromEnv(), 'GetOrdersQueryCache')

lib/repositories/QueryCache.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { OrderEntityType, QueryResult } from './base'
2+
3+
type CacheEntry<T> = {
4+
expiresAt: number
5+
value: T
6+
}
7+
8+
const DEFAULT_QUERY_CACHE_TTL_MS = 250
9+
10+
// Set GET_ORDERS_CACHE_TTL_MS=0 to disable the query cache without a code change.
11+
export function queryCacheTtlFromEnv(): number {
12+
const raw = process.env.GET_ORDERS_CACHE_TTL_MS
13+
if (!raw) {
14+
return DEFAULT_QUERY_CACHE_TTL_MS
15+
}
16+
const configured = Number(raw)
17+
return Number.isFinite(configured) && configured >= 0 ? configured : DEFAULT_QUERY_CACHE_TTL_MS
18+
}
19+
20+
/**
21+
* Process-local TTL cache for read-only DynamoDB query results.
22+
*
23+
* Fillers poll the same chainId/orderStatus combinations continuously, so within a few
24+
* hundred milliseconds many requests ask for a byte-identical page. Those queries all
25+
* land on a single GSI partition key (e.g. `1_open`), which DynamoDB caps at ~3000 RCU/s
26+
* regardless of the table's billing mode. Serving repeats from memory collapses them into
27+
* one read and keeps the partition below that ceiling.
28+
*
29+
* Query keys embed caller-supplied values (swapper, pair, cursor), so the map is bounded:
30+
* an unbounded one would be a memory leak in a long-lived execution environment.
31+
*/
32+
export class QueryCache<T> {
33+
private readonly store = new Map<string, CacheEntry<T>>()
34+
35+
// metricPrefix names the hit/miss metrics emitted by repositories using this cache
36+
// (e.g. 'GetOrdersQueryCache' -> GetOrdersQueryCacheHit/Miss). Each endpoint owns its
37+
// cache instance, so its traffic stays distinguishable on dashboards.
38+
constructor(
39+
private readonly ttlMs: number,
40+
public readonly metricPrefix: string,
41+
private readonly maxEntries = 1000
42+
) {}
43+
44+
public get enabled(): boolean {
45+
return this.ttlMs > 0
46+
}
47+
48+
public get(key: string, now: number): T | undefined {
49+
if (!this.enabled) {
50+
return undefined
51+
}
52+
const entry = this.store.get(key)
53+
if (!entry) {
54+
return undefined
55+
}
56+
if (entry.expiresAt <= now) {
57+
this.store.delete(key)
58+
return undefined
59+
}
60+
return entry.value
61+
}
62+
63+
public set(key: string, value: T, now: number): void {
64+
if (!this.enabled) {
65+
return
66+
}
67+
this.evict(now)
68+
// Re-inserting an existing key keeps its original position in a Map, which would break
69+
// the insertion-order-equals-expiry-order invariant evict() relies on. Delete first.
70+
this.store.delete(key)
71+
this.store.set(key, { expiresAt: now + this.ttlMs, value })
72+
}
73+
74+
public get size(): number {
75+
return this.store.size
76+
}
77+
78+
public clear(): void {
79+
this.store.clear()
80+
}
81+
82+
/**
83+
* Every entry gets the same TTL, so insertion order is also expiry order: we can stop
84+
* sweeping at the first live entry, and drop from the front when over capacity.
85+
*/
86+
private evict(now: number): void {
87+
for (const [key, entry] of this.store) {
88+
if (entry.expiresAt > now) {
89+
break
90+
}
91+
this.store.delete(key)
92+
}
93+
while (this.store.size >= this.maxEntries) {
94+
const oldest = this.store.keys().next()
95+
if (oldest.done) {
96+
break
97+
}
98+
this.store.delete(oldest.value)
99+
}
100+
}
101+
}
102+
103+
export type OrdersQueryCache = QueryCache<QueryResult<OrderEntityType>>

lib/repositories/RelayOrderRepository.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import { RelayOrderEntity } from '../entities'
77
import { BaseOrdersRepository, MODEL_NAME } from './base'
88
import { GenericOrdersRepository } from './generic-orders-repository'
99
import { OffchainOrderIndexMapper } from './IndexMappers/OffchainOrderIndexMapper'
10+
import { OrdersQueryCache } from './QueryCache'
1011
import { getTableIndices, TABLE_NAMES } from './util'
1112

1213
export class RelayOrderRepository extends GenericOrdersRepository<string, string, null, RelayOrderEntity> {
13-
static create(documentClient: DocumentClient): BaseOrdersRepository<RelayOrderEntity> {
14+
static create(documentClient: DocumentClient, queryCache?: OrdersQueryCache): BaseOrdersRepository<RelayOrderEntity> {
1415
const log = Logger.createLogger({
1516
name: 'RelayOrdersRepository',
1617
serializers: Logger.stdSerializers,
@@ -76,7 +77,8 @@ export class RelayOrderRepository extends GenericOrdersRepository<string, string
7677
relayOrderEntity,
7778
nonceEntity,
7879
log,
79-
new OffchainOrderIndexMapper<RelayOrderEntity>()
80+
new OffchainOrderIndexMapper<RelayOrderEntity>(),
81+
queryCache
8082
)
8183
}
8284
}

0 commit comments

Comments
 (0)