Skip to content

Commit 25e7206

Browse files
committed
refactor: drop reaper status param for terminal-set guard; simplify repository condition per review
1 parent b3c9429 commit 25e7206

3 files changed

Lines changed: 22 additions & 36 deletions

File tree

lib/crons/gs-reaper/gs-reaper.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { BLOCK_RANGE, REAPER_MAX_ATTEMPTS, DYNAMO_BATCH_WRITE_MAX, OLDEST_BLOCK_
88
import { ethers } from 'ethers'
99
import { CosignedPriorityOrder, CosignedV2DutchOrder, CosignedV3DutchOrder, DutchOrder, FillInfo, CosignedHybridOrder, OrderType, OrderValidation, OrderValidator, REACTOR_ADDRESS_MAPPING, UniswapXEventWatcher, UniswapXOrder } from '@uniswap/uniswapx-sdk'
1010
import { parseOrder } from '../../handlers/OrderParser'
11-
import { AVERAGE_BLOCK_TIME, getSettledAmounts } from '../../handlers/check-order-status/util'
11+
import { AVERAGE_BLOCK_TIME, getSettledAmounts, IS_TERMINAL_STATE } from '../../handlers/check-order-status/util'
1212
import { ChainId } from '../../util/chain'
1313
import { getRpcUrl } from '../../Config'
1414
import { LimitOrdersRepository } from '../../repositories/limit-orders-repository'
@@ -229,8 +229,7 @@ export class GSReaper {
229229
provider,
230230
state.chainId,
231231
this.log,
232-
state.failedFillScanRanges,
233-
this.unresolvedOrderStatus
232+
state.failedFillScanRanges
234233
)
235234

236235
return {
@@ -413,8 +412,7 @@ async function checkCancelledOrders(
413412
provider: ethers.providers.StaticJsonRpcProvider,
414413
chainId: number,
415414
log: Logger,
416-
failedFillScanRanges: BlockRange[],
417-
unresolvedOrderStatus: ORDER_STATUS,
415+
failedFillScanRanges: BlockRange[]
418416
): Promise<Record<string, OrderUpdate>> {
419417
const orderUpdates = { ...existingUpdates }
420418
const quoter = new OrderValidator(provider, chainId)
@@ -463,14 +461,10 @@ async function checkCancelledOrders(
463461
if (!orderUpdates[orderHash]) {
464462
try {
465463
const { order, signature, entity } = await getOrderByHash(repo, orderHash)
466-
// Another writer (e.g. the check-order-status state machine) may have
467-
// resolved this order since the run's GET_OPEN_ORDERS snapshot -- most
468-
// importantly to FILLED, which a used nonce is also consistent with.
469-
// Only resolve orders whose DB status still matches the snapshot.
470-
if (entity.orderStatus !== unresolvedOrderStatus) {
471-
log.info(
472-
`Order ${orderHash} status is now ${entity.orderStatus} (no longer ${unresolvedOrderStatus}); skipping resolution`
473-
)
464+
// Another writer (e.g. check-order-status) may have resolved this order since
465+
// the run's GET_OPEN_ORDERS snapshot; never re-resolve a terminal order.
466+
if (IS_TERMINAL_STATE(entity.orderStatus)) {
467+
log.info(`Order ${orderHash} is already terminal (${entity.orderStatus}); skipping resolution`)
474468
continue
475469
}
476470
// We only check for nonce used and expired for permissioned tokens

lib/repositories/generic-orders-repository.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,9 @@ import { IndexMapper } from './IndexMappers/IndexMapper'
1515

1616
export const MAX_ORDERS = 50
1717

18+
// aws-sdk v2 (used by dynamodb-toolbox here) sets both code and name to the error code
1819
function isConditionalCheckFailed(e: unknown): boolean {
19-
return (
20-
typeof e === 'object' &&
21-
e !== null &&
22-
((e as { code?: string }).code === 'ConditionalCheckFailedException' ||
23-
(e as { name?: string }).name === 'ConditionalCheckFailedException')
24-
)
20+
return (e as { code?: string } | null)?.code === 'ConditionalCheckFailedException'
2521
}
2622

2723
// Shared implementation for Dutch and Limit orders
@@ -124,6 +120,11 @@ export abstract class GenericOrdersRepository<
124120
`cannot find order by hash when updating order status, hash: ${orderHash}`
125121
)
126122

123+
// FILLED is terminal; the DynamoDB condition makes the no-downgrade check atomic with the write
124+
const conditions =
125+
status === ORDER_STATUS.FILLED
126+
? {}
127+
: { conditions: [{ attr: TABLE_KEY.ORDER_STATUS, ne: ORDER_STATUS.FILLED }] }
127128
await this.entity.update(
128129
{
129130
[TABLE_KEY.ORDER_HASH]: orderHash,
@@ -132,18 +133,11 @@ export abstract class GenericOrdersRepository<
132133
...(fillBlock && { fillBlock }),
133134
...(settledAmounts && { settledAmounts })
134135
},
135-
// FILLED is terminal: once an order is recorded as filled, no writer may
136-
// downgrade it to another status (e.g. a reaper run whose fill scan
137-
// predates the fill misreading its used nonce as a cancellation).
138-
// Enforced as a DynamoDB condition so the check is atomic with the write.
139-
status === ORDER_STATUS.FILLED
140-
? {}
141-
: { conditions: [{ attr: TABLE_KEY.ORDER_STATUS, ne: ORDER_STATUS.FILLED }] }
136+
conditions
142137
)
143138
} catch (e) {
144139
if (isConditionalCheckFailed(e)) {
145-
// The order reached FILLED between our read and this write; the update
146-
// would downgrade a terminal status, so skip it rather than retry.
140+
// the order was FILLED by another writer since our read; skip the downgrade rather than throw
147141
log.warn('skipping updateOrderStatus: order is already in terminal status FILLED', { orderHash, status })
148142
return
149143
}

test/unit/handlers/gs-reaper/gs-reaper.test.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -352,14 +352,12 @@ describe('GSReaper', () => {
352352
expect(result?.orderUpdates[MOCK_ORDER_ENTITY.orderHash].status).toBe(ORDER_STATUS.CANCELLED)
353353
})
354354

355-
it('does NOT resolve an order whose DB status changed since the run snapshot', async () => {
356-
// Regression (PROTO-1201): the run's order-hash snapshot is taken in
357-
// GET_OPEN_ORDERS, but another writer (e.g. the check-order-status state
358-
// machine) can resolve the order -- most importantly to FILLED -- before
359-
// CHECK_CANCELLED validates it. A used nonce is consistent with that
360-
// fill, so the reaper must re-check the CURRENT DB status and skip
361-
// orders that already moved on, instead of clobbering FILLED with
362-
// CANCELLED.
355+
it('does NOT resolve an order whose DB status is already terminal', async () => {
356+
// Regression (PROTO-1201): another writer (e.g. the check-order-status
357+
// state machine) can resolve the order to FILLED between the run's
358+
// GET_OPEN_ORDERS snapshot and CHECK_CANCELLED. A used nonce is
359+
// consistent with that fill, so the reaper must skip already-terminal
360+
// orders instead of clobbering FILLED with CANCELLED.
363361
await mockOrdersRepository.addOrder({
364362
...MOCK_ORDER_ENTITY,
365363
orderStatus: ORDER_STATUS.FILLED,

0 commit comments

Comments
 (0)