Skip to content

Commit 5fc9154

Browse files
codybornclaude
andauthored
fix: lazy per-chain provider/validator construction to fix PC OOM (#665)
* fix: lazy per-chain provider/validator construction to fix PC OOM PostOrder/PostLimitOrder/CheckOrderStatus were constructing one StaticJsonRpcProvider plus one or more OnChain*Validator instances per SUPPORTED_CHAIN at module load. After SUPPORTED_CHAINS grew from 7 to 18 in #654, provisioned-concurrency pre-warm OOMed again ("Provisioned Concurrency configuration failed to be applied. Reason: FAILED"), undoing the headroom that the provider-dedup fix in #663 had just carved out. Decouple cold-start memory from chain count by constructing providers and validators on first per-chain access instead of eagerly at module load: - OnChainValidatorMap accepts an optional { factory, isSupported } options object. get() lazily constructs and caches via the factory; has() consults isSupported. Existing positional initial-array constructor and set() API are preserved so tests and call sites that pass a pre-built mapping are unchanged. - ProviderMap is loosened from Map<ChainId, StaticJsonRpcProvider> to a structural interface { get(chainId): StaticJsonRpcProvider | undefined }; Map still satisfies it. New LazyProviderMap class constructs one provider per chain on first access and caches it. - post-order, post-limit-order, and check-order-status drop their SUPPORTED_CHAINS module-load loops in favor of lazy maps. Cold start now allocates zero providers and zero validators; first request to a chain pays a one-time alloc. Also fixes a latent bug in post-limit-order/index.ts where the second SUPPORTED_CHAINS loop was populating onChainValidatorMap a second time instead of relayOrderValidatorMap, leaving relay orders on the limit route without an on-chain validator. The factory rewrite removes the duplicated loop entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: replace ! assertions in factories with LazyProviderMap.getOrThrow The factory closures all used providerMap.get(chainId)! to coerce the optional return into a non-null provider. That's safe today because every validator map's isSupported predicate is a subset of LazyProviderMap's supported set, but it's a brittle invariant — narrowing the supported set in a future change would silently produce undefined providers wrapped inside validator instances. Add LazyProviderMap.getOrThrow(chainId): StaticJsonRpcProvider that throws a clear error if the chainId isn't supported, and use it from the four factory closures. The structural ProviderMap interface (Map-compatible .get returning T | undefined) is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(lambda): bump prod PostOrder/PostLimitOrder memorySize 1024 -> 2048 MB Defense-in-depth on top of the lazy provider/validator init in this PR. Lazy construction eliminates the cold-start spike, but prod runs ~50x the traffic of beta and will reach steady-state with all 18 SUPPORTED_CHAINS warmed in memory faster than beta. Doubling memory keeps PC pre-warm and steady-state allocations well clear of the OOM boundary that's bitten us once already at 18 chains. Beta stays at 1024 MB so it remains a fair canary for whether the lazy fix is sufficient on its own at future chain counts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: drop isSupported gate; trust boundary SUPPORTED_CHAINS validation The HTTP request boundary already enforces chainId ∈ SUPPORTED_CHAINS via Joi (lib/util/field-validator.ts:59, CHAIN_ID_JOI). Step Functions events in check-order-status are seeded by post-order, which inherits that validation. With the boundary trusted, the duplicated isSupported predicates in OnChainValidatorMap factories and the supported set in LazyProviderMap were dead weight. - OnChainValidatorMap: drop OnChainValidatorMapOptions and the has() method. Factory now passed positionally; get() always calls it on cache miss. has() had a single caller (V4 quoter check in UniswapXOrderService) handled below. - LazyProviderMap: drop the supported constructor param and getOrThrow(). get() now constructs+caches unconditionally and returns a StaticJsonRpcProvider non-null. The ProviderMap structural interface still allows undefined for Map<> compatibility. - UniswapXOrderService: replace this.onChainV4ValidatorMap?.has(chainId) with a direct UNISWAPX_V4_ORDER_QUOTER_MAPPING[chainId] lookup. The V4 quoter mapping is a property of the SDK + per-chain reactor deploys, not of the validator-map abstraction — keeping it at the call site removes the abstraction's only V4-specific concern. - Handlers: drop the supportedChainSet/isSupportedChain boilerplate; factory closures pass providerMap.get(chainId) directly. Net: -37 lines, no remaining duplicate "is this chain supported" logic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5e4878d commit 5fc9154

7 files changed

Lines changed: 72 additions & 82 deletions

File tree

bin/stacks/lambda-stack.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,15 @@ export class LambdaStack extends cdk.NestedStack {
215215
postOrderEnv[`STATE_MACHINE_ARN_${chainId}`] = sfnStack.chainIdToStatusTrackingStateMachineArn[chainId]
216216
})
217217

218+
const postOrderMemorySize = props.stage === STAGE.PROD ? 2048 : 1024
219+
218220
this.postOrderLambda = new aws_lambda_nodejs.NodejsFunction(this, `PostOrder${lambdaName}`, {
219221
role: lambdaRole,
220222
runtime: aws_lambda.Runtime.NODEJS_20_X,
221223
entry: path.join(__dirname, '../../lib/handlers/post-order/index.ts'),
222224
handler: 'postOrderHandler',
223225
timeout: Duration.seconds(29),
224-
memorySize: 1024,
226+
memorySize: postOrderMemorySize,
225227
bundling: {
226228
minify: true,
227229
sourceMap: true,
@@ -237,7 +239,7 @@ export class LambdaStack extends cdk.NestedStack {
237239
entry: path.join(__dirname, '../../lib/handlers/post-limit-order/index.ts'),
238240
handler: 'postLimitOrderHandler',
239241
timeout: Duration.seconds(29),
240-
memorySize: 1024,
242+
memorySize: postOrderMemorySize,
241243
bundling: {
242244
minify: true,
243245
sourceMap: true,

lib/handlers/OnChainValidatorMap.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,28 @@ import { ChainId } from '../util/chain'
33

44
export class OnChainValidatorMap<T extends OrderValidator | RelayOrderValidator | V4OrderValidator> {
55
private chainIdToValidators: Map<ChainId, T> = new Map()
6+
private readonly factory?: (chainId: ChainId) => T
67

7-
constructor(initial: Array<[ChainId, T]> = []) {
8+
constructor(initial: Array<[ChainId, T]> = [], factory?: (chainId: ChainId) => T) {
89
for (const [chainId, validator] of initial) {
910
this.chainIdToValidators.set(chainId, validator)
1011
}
12+
this.factory = factory
1113
}
1214

1315
get(chainId: ChainId): T {
14-
const validator = this.chainIdToValidators.get(chainId)
16+
let validator = this.chainIdToValidators.get(chainId)
17+
if (!validator && this.factory) {
18+
validator = this.factory(chainId)
19+
this.chainIdToValidators.set(chainId, validator)
20+
}
1521
if (!validator) {
1622
throw new Error(`No onchain validator for chain ${chainId}`)
1723
}
1824

1925
return validator
2026
}
2127

22-
has(chainId: ChainId): boolean {
23-
return this.chainIdToValidators.has(chainId)
24-
}
25-
2628
set(chainId: ChainId, validator: T): void {
2729
this.chainIdToValidators.set(chainId, validator)
2830
}

lib/handlers/check-order-status/index.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,30 @@
11
import { OrderType, RelayOrderValidator as OnChainRelayOrderValidator } from '@uniswap/uniswapx-sdk'
22
import { DynamoDB } from 'aws-sdk'
33
import { DocumentClient } from 'aws-sdk/clients/dynamodb'
4-
import { ethers } from 'ethers'
5-
import { CONFIG } from '../../Config'
64
import { log } from '../../Logging'
75
import { DutchOrdersRepository } from '../../repositories/dutch-orders-repository'
86
import { LimitOrdersRepository } from '../../repositories/limit-orders-repository'
97
import { RelayOrderRepository } from '../../repositories/RelayOrderRepository'
108
import { AnalyticsService } from '../../services/analytics-service'
119
import { RelayOrderService } from '../../services/RelayOrderService'
12-
import { SUPPORTED_CHAINS } from '../../util/chain'
1310
import { OffChainRelayOrderValidator } from '../../util/OffChainRelayOrderValidator'
1411
import { FillEventLogger } from '../check-order-status/fill-event-logger'
1512
import { calculateDutchRetryWaitSeconds, FILL_EVENT_LOOKBACK_BLOCKS_ON } from '../check-order-status/util'
1613
import { EventWatcherMap } from '../EventWatcherMap'
1714
import { OnChainValidatorMap } from '../OnChainValidatorMap'
15+
import { LazyProviderMap } from '../shared'
1816
import { getMaxOpenOrders } from '../post-order/injector'
1917
import { CheckOrderStatusHandler } from './handler'
2018
import { CheckOrderStatusInjector } from './injector'
2119
import { CheckOrderStatusService, CheckOrderStatusUtils } from './service'
22-
import { RPC_HEADERS } from '../../util/constants'
20+
21+
const providerMap = new LazyProviderMap()
2322

2423
const relayOrderValidator = new OffChainRelayOrderValidator(() => new Date().getTime() / 1000)
25-
const relayOrderValidatorMap = new OnChainValidatorMap<OnChainRelayOrderValidator>()
26-
for (const chainId of SUPPORTED_CHAINS) {
27-
relayOrderValidatorMap.set(
28-
chainId,
29-
new OnChainRelayOrderValidator(new ethers.providers.StaticJsonRpcProvider({
30-
url: CONFIG.rpcUrls.get(chainId),
31-
headers: RPC_HEADERS
32-
}, chainId), chainId)
33-
)
34-
}
24+
const relayOrderValidatorMap = new OnChainValidatorMap<OnChainRelayOrderValidator>(
25+
[],
26+
(chainId) => new OnChainRelayOrderValidator(providerMap.get(chainId), chainId)
27+
)
3528

3629
const relayOrderService = new RelayOrderService(
3730
relayOrderValidator,

lib/handlers/post-limit-order/index.ts

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,14 @@ import {
33
RelayOrderValidator as OnChainRelayOrderValidator,
44
} from '@uniswap/uniswapx-sdk'
55
import { DynamoDB } from 'aws-sdk'
6-
import { ethers } from 'ethers'
7-
import { CONFIG } from '../../Config'
86
import { log } from '../../Logging'
97
import { LimitOrdersRepository } from '../../repositories/limit-orders-repository'
108
import { RelayOrderRepository } from '../../repositories/RelayOrderRepository'
119
import { AnalyticsService } from '../../services/analytics-service'
1210
import { OrderDispatcher } from '../../services/OrderDispatcher'
1311
import { RelayOrderService } from '../../services/RelayOrderService'
1412
import { UniswapXOrderService } from '../../services/UniswapXOrderService'
15-
import { SUPPORTED_CHAINS } from '../../util/chain'
16-
import { ONE_YEAR_IN_SECONDS, RPC_HEADERS } from '../../util/constants'
13+
import { ONE_YEAR_IN_SECONDS } from '../../util/constants'
1714
import { OffChainRelayOrderValidator } from '../../util/OffChainRelayOrderValidator'
1815
import { OffChainUniswapXOrderValidator } from '../../util/OffChainUniswapXOrderValidator'
1916
import { FillEventLogger } from '../check-order-status/fill-event-logger'
@@ -22,30 +19,21 @@ import { EventWatcherMap } from '../EventWatcherMap'
2219
import { OnChainValidatorMap } from '../OnChainValidatorMap'
2320
import { PostOrderHandler } from '../post-order/handler'
2421
import { PostOrderBodyParser } from '../post-order/PostOrderBodyParser'
25-
import { ProviderMap } from '../shared'
22+
import { LazyProviderMap } from '../shared'
2623
import { getMaxLimitOpenOrders, PostLimitOrderInjector } from './injector'
2724
import { DynamoQuoteMetadataRepository } from '../../repositories/quote-metadata-repository'
2825

29-
const onChainValidatorMap = new OnChainValidatorMap<OnChainOrderValidator>()
26+
const providerMap = new LazyProviderMap()
3027

31-
for (const chainId of SUPPORTED_CHAINS) {
32-
onChainValidatorMap.set(
33-
chainId,
34-
new OnChainOrderValidator(new ethers.providers.StaticJsonRpcProvider({
35-
url: CONFIG.rpcUrls.get(chainId),
36-
headers: RPC_HEADERS
37-
}, chainId), chainId)
38-
)
39-
}
40-
41-
const providerMap: ProviderMap = new Map()
28+
const onChainValidatorMap = new OnChainValidatorMap<OnChainOrderValidator>(
29+
[],
30+
(chainId) => new OnChainOrderValidator(providerMap.get(chainId), chainId)
31+
)
4232

43-
for (const chainId of SUPPORTED_CHAINS) {
44-
providerMap.set(chainId, new ethers.providers.StaticJsonRpcProvider({
45-
url: CONFIG.rpcUrls.get(chainId),
46-
headers: RPC_HEADERS
47-
}, chainId))
48-
}
33+
const relayOrderValidatorMap = new OnChainValidatorMap<OnChainRelayOrderValidator>(
34+
[],
35+
(chainId) => new OnChainRelayOrderValidator(providerMap.get(chainId), chainId)
36+
)
4937

5038
const orderValidator = new OffChainUniswapXOrderValidator(() => new Date().getTime() / 1000, ONE_YEAR_IN_SECONDS, {
5139
SkipDecayStartTimeValidation: true,
@@ -68,17 +56,7 @@ const uniswapXOrderService = new UniswapXOrderService(
6856
)
6957

7058
const relayOrderValidator = new OffChainRelayOrderValidator(() => new Date().getTime() / 1000)
71-
const relayOrderValidatorMap = new OnChainValidatorMap<OnChainRelayOrderValidator>()
7259

73-
for (const chainId of SUPPORTED_CHAINS) {
74-
onChainValidatorMap.set(
75-
chainId,
76-
new OnChainOrderValidator(new ethers.providers.StaticJsonRpcProvider({
77-
url: CONFIG.rpcUrls.get(chainId),
78-
headers: RPC_HEADERS
79-
}, chainId), chainId)
80-
)
81-
}
8260
const relayOrderService = new RelayOrderService(
8361
relayOrderValidator,
8462
relayOrderValidatorMap,

lib/handlers/post-order/index.ts

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ import {
22
OrderValidator as OnChainOrderValidator,
33
RelayOrderValidator as OnChainRelayOrderValidator,
44
V4OrderValidator as OnChainV4OrderValidator,
5-
UNISWAPX_V4_ORDER_QUOTER_MAPPING as OnChainV4QuoterMapping
65
} from '@uniswap/uniswapx-sdk'
76
import { DynamoDB } from 'aws-sdk'
8-
import { ethers } from 'ethers'
9-
import { CONFIG } from '../../Config'
107
import { log } from '../../Logging'
118
import { DutchOrdersRepository } from '../../repositories/dutch-orders-repository'
129
import { LimitOrdersRepository } from '../../repositories/limit-orders-repository'
@@ -20,34 +17,34 @@ import { S3WebhookConfigurationProvider } from '../../providers/s3-webhook-provi
2017
import { BETA_WEBHOOK_CONFIG_KEY, PRODUCTION_WEBHOOK_CONFIG_KEY, WEBHOOK_CONFIG_BUCKET } from '../../util/constants'
2118
import { STAGE } from '../../util/stage'
2219
import { checkDefined } from '../../preconditions/preconditions'
23-
import { SUPPORTED_CHAINS } from '../../util/chain'
24-
import { ONE_DAY_IN_SECONDS, RPC_HEADERS } from '../../util/constants'
20+
import { ONE_DAY_IN_SECONDS } from '../../util/constants'
2521
import { OffChainRelayOrderValidator } from '../../util/OffChainRelayOrderValidator'
2622
import { OffChainUniswapXOrderValidator } from '../../util/OffChainUniswapXOrderValidator'
2723
import { FillEventLogger } from '../check-order-status/fill-event-logger'
2824
import { FILL_EVENT_LOOKBACK_BLOCKS_ON } from '../check-order-status/util'
2925
import { EventWatcherMap } from '../EventWatcherMap'
3026
import { OnChainValidatorMap } from '../OnChainValidatorMap'
31-
import { ProviderMap } from '../shared/'
27+
import { LazyProviderMap } from '../shared/'
3228
import { PostOrderHandler } from './handler'
3329
import { getMaxOpenOrders, PostOrderInjector } from './injector'
3430
import { PostOrderBodyParser } from './PostOrderBodyParser'
3531

36-
const onChainValidatorMap = new OnChainValidatorMap<OnChainOrderValidator>()
37-
const onChainV4ValidatorMap = new OnChainValidatorMap<OnChainV4OrderValidator>()
38-
const providerMap: ProviderMap = new Map()
32+
const providerMap = new LazyProviderMap()
3933

40-
for (const chainId of SUPPORTED_CHAINS) {
41-
const provider = new ethers.providers.StaticJsonRpcProvider(
42-
{ url: CONFIG.rpcUrls.get(chainId), headers: RPC_HEADERS },
43-
chainId
44-
)
45-
providerMap.set(chainId, provider)
46-
onChainValidatorMap.set(chainId, new OnChainOrderValidator(provider, chainId))
47-
if (OnChainV4QuoterMapping[chainId]) {
48-
onChainV4ValidatorMap.set(chainId, new OnChainV4OrderValidator(provider, chainId))
49-
}
50-
}
34+
const onChainValidatorMap = new OnChainValidatorMap<OnChainOrderValidator>(
35+
[],
36+
(chainId) => new OnChainOrderValidator(providerMap.get(chainId), chainId)
37+
)
38+
39+
const onChainV4ValidatorMap = new OnChainValidatorMap<OnChainV4OrderValidator>(
40+
[],
41+
(chainId) => new OnChainV4OrderValidator(providerMap.get(chainId), chainId)
42+
)
43+
44+
const relayOrderValidatorMap = new OnChainValidatorMap<OnChainRelayOrderValidator>(
45+
[],
46+
(chainId) => new OnChainRelayOrderValidator(providerMap.get(chainId), chainId)
47+
)
5148

5249
const postOrderInjectorPromise = new PostOrderInjector('postOrderInjector').build()
5350

@@ -76,10 +73,6 @@ const uniswapXOrderService = new UniswapXOrderService(
7673
)
7774

7875
const relayOrderValidator = new OffChainRelayOrderValidator(() => new Date().getTime() / 1000)
79-
const relayOrderValidatorMap = new OnChainValidatorMap<OnChainRelayOrderValidator>()
80-
for (const chainId of SUPPORTED_CHAINS) {
81-
relayOrderValidatorMap.set(chainId, new OnChainRelayOrderValidator(providerMap.get(chainId)!, chainId))
82-
}
8376

8477
const relayOrderService = new RelayOrderService(
8578
relayOrderValidator,

lib/handlers/shared/index.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,25 @@
11
import { StaticJsonRpcProvider } from '@ethersproject/providers'
2-
import { SUPPORTED_CHAINS } from '../../util/chain'
2+
import { ethers } from 'ethers'
3+
import { CONFIG } from '../../Config'
4+
import { ChainId } from '../../util/chain'
5+
import { RPC_HEADERS } from '../../util/constants'
36

4-
export type ProviderMap = Map<typeof SUPPORTED_CHAINS[number], StaticJsonRpcProvider>
7+
export interface ProviderMap {
8+
get(chainId: ChainId): StaticJsonRpcProvider | undefined
9+
}
10+
11+
export class LazyProviderMap implements ProviderMap {
12+
private readonly providers: Map<ChainId, StaticJsonRpcProvider> = new Map()
13+
14+
get(chainId: ChainId): StaticJsonRpcProvider {
15+
let provider = this.providers.get(chainId)
16+
if (!provider) {
17+
provider = new ethers.providers.StaticJsonRpcProvider(
18+
{ url: CONFIG.rpcUrls.get(chainId), headers: RPC_HEADERS },
19+
chainId
20+
)
21+
this.providers.set(chainId, provider)
22+
}
23+
return provider
24+
}
25+
}

lib/services/UniswapXOrderService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
OrderValidator as OnChainOrderValidator,
1313
PermissionedTokenValidator,
1414
V4OrderValidator as OnChainV4OrderValidator,
15+
UNISWAPX_V4_ORDER_QUOTER_MAPPING as OnChainV4QuoterMapping,
1516
} from '@uniswap/uniswapx-sdk'
1617
import { ethers } from 'ethers'
1718
import { ORDER_STATUS, UniswapXOrderEntity } from '../entities'
@@ -208,7 +209,7 @@ export class UniswapXOrderService {
208209

209210
// Use V4 quoter for Hybrid orders if available on this chain
210211
if (order instanceof CosignedHybridOrder) {
211-
if (this.onChainV4ValidatorMap?.has(chainId)) {
212+
if (this.onChainV4ValidatorMap && OnChainV4QuoterMapping[chainId]) {
212213
const onChainV4Validator = this.onChainV4ValidatorMap.get(chainId)
213214
onChainValidationResult = await onChainV4Validator.validate({ order: order, signature: signature })
214215
} else {

0 commit comments

Comments
 (0)