Skip to content

Commit 62acc02

Browse files
authored
chore(ymax-planner): Improve use of rejectPlan (#12290)
Ref #12274 Ref #12235 (comment) ## Description * Don't treat successful `rejectPlan` as an error * Include log output for `rejectPlan` * Consolidate startFlow log output * Don't update portfolio state snapshots when handlePortfolio fails ### Security Considerations None known. ### Scaling Considerations n/a ### Documentation Considerations n/a ### Testing Considerations Covered. ### Upgrade Considerations This affects only happy-path output that AFAIK nothing specifically looks for, so should be safe to deploy immediately.
2 parents a85c730 + 251decc commit 62acc02

File tree

2 files changed

+81
-78
lines changed

2 files changed

+81
-78
lines changed

services/ymax-planner/src/engine.ts

Lines changed: 39 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,14 @@ export const processPortfolioEvents = async (
288288
const flowId = flowIdFromKey(flowKey);
289289
const scope = [portfolioId, flowId] as const;
290290
const { policyVersion, rebalanceCount, targetAllocation } = portfolioStatus;
291-
const conditions = [policyVersion, rebalanceCount] as const;
291+
const versions = [policyVersion, rebalanceCount] as const;
292292

293293
const currentBalances = await getNonDustBalances(
294294
portfolioStatus,
295295
depositBrand,
296296
balanceQueryPowers,
297297
);
298-
const errorContext = {
298+
const logContext = {
299299
path,
300300
flowKey,
301301
flowDetail,
@@ -304,40 +304,37 @@ export const processPortfolioEvents = async (
304304
rebalanceCount,
305305
targetAllocation,
306306
};
307-
const plannerContext = {
308-
...errorContext,
309-
// @ts-expect-error "amount" is not present on all varieties of
310-
// FlowDetail, but we need it here when it is present (i.e., for types
311-
// "deposit" and "withdraw" and it's harmless otherwise.
312-
amount: flowDetail.amount,
313-
network,
314-
brand: depositBrand,
315-
feeBrand,
316-
gasEstimator,
317-
};
318-
319-
const { network: _network, ...logContext } = plannerContext;
320-
logger.debug(`Starting flow`, flowDetail, inspectForStdout(logContext));
321307
const settle = async <M extends string & keyof PortfolioPlanner>(
322308
methodName: M,
323309
args: PortfolioPlanner[M] extends (...args: infer Args) => any
324310
? Args
325311
: never,
312+
extraDetails?: object,
326313
) => {
327-
const planReceiver = walletStore.get<PortfolioPlanner>('planner', {
328-
sendOnly: true,
329-
});
314+
const txOpts = { sendOnly: true };
315+
const planReceiver = walletStore.get<PortfolioPlanner>('planner', txOpts);
330316
const { tx, id } = await planReceiver[methodName]!(...args);
331-
// The transaction has been submitted, but we won't know about a rejection
332-
// for at least another block.
317+
// tx has been submitted, but we won't know its fate until a future block.
333318
if (!isDryRun) {
334319
void getWalletInvocationUpdate(id as any).catch(err => {
335320
logger.warn(`⚠️ Failure for ${methodName}`, args, err);
336321
});
337322
}
338-
return tx;
323+
const details = inspectForStdout({ ...logContext, ...extraDetails });
324+
logger.info(methodName, flowDetail, currentBalances, details, tx);
339325
};
340326

327+
const plannerContext = {
328+
...logContext,
329+
// @ts-expect-error "amount" is not present on all varieties of
330+
// FlowDetail, but we need it here when it is present (i.e., for types
331+
// "deposit" and "withdraw" and it's harmless otherwise.
332+
amount: flowDetail.amount,
333+
network,
334+
brand: depositBrand,
335+
feeBrand,
336+
gasEstimator,
337+
};
341338
try {
342339
let steps: MovementDesc[];
343340
const { type } = flowDetail;
@@ -351,43 +348,29 @@ export const processPortfolioEvents = async (
351348
case 'withdraw':
352349
steps = await planWithdrawFromAllocations(plannerContext);
353350
break;
354-
default: {
351+
default:
355352
logger.warn(`⚠️ Unknown flow type ${type}`);
356353
return;
357-
}
358354
}
359-
(errorContext as any).steps = steps;
360-
361-
const tx = await (steps.length === 0
362-
? settle('rejectPlan', [
363-
...scope,
364-
'Nothing to do for this operation.',
365-
...conditions,
366-
])
367-
: settle('resolvePlan', [...scope, steps, ...conditions]));
368-
logger.info(
369-
`Resolving`,
370-
flowDetail,
371-
currentBalances,
372-
inspectForStdout({
373-
policyVersion,
374-
rebalanceCount,
375-
targetAllocation,
376-
steps,
377-
}),
378-
tx,
379-
);
355+
(logContext as any).steps = steps;
356+
357+
if (steps.length > 0) {
358+
await settle('resolvePlan', [...scope, steps, ...versions], { steps });
359+
} else {
360+
const reason = 'Nothing to do for this operation.';
361+
await settle('rejectPlan', [...scope, reason, ...versions]);
362+
}
380363
} catch (err) {
364+
annotateError(err, inspect(logContext, { depth: 4 }));
381365
if (err instanceof UserInputError || err instanceof NoSolutionError) {
382-
try {
383-
await settle('rejectPlan', [...scope, err.message, ...conditions]);
384-
} catch (settleErr) {
385-
// eslint-disable-next-line no-ex-assign
386-
err = AggregateError([err, settleErr]);
387-
}
366+
await settle('rejectPlan', [...scope, err.message, ...versions], {
367+
cause: err,
368+
}).catch(err2 => {
369+
throw AggregateError([err, err2]);
370+
});
371+
} else {
372+
throw err;
388373
}
389-
annotateError(err, inspect(errorContext, { depth: 4 }));
390-
throw err;
391374
}
392375
};
393376
const handledPortfolioKeys = new Set<string>();
@@ -413,7 +396,7 @@ export const processPortfolioEvents = async (
413396
}
414397

415398
// If this (portfolio, flows) data hasn't changed since our last
416-
// submission, there's no point in trying again.
399+
// successful submission, there's no point in trying again.
417400
memory.snapshots ||= new Map();
418401
const oldState = memory.snapshots.get(portfolioKey);
419402
const oldFingerprint = oldState?.fingerprint;
@@ -424,7 +407,6 @@ export const processPortfolioEvents = async (
424407
oldState.repeats += 1;
425408
return;
426409
}
427-
memory.snapshots.set(portfolioKey, { fingerprint, repeats: 0 });
428410

429411
// If any in-progress flows need activation (as indicated by not having
430412
// its own dedicated vstorage data), then find the first such flow and
@@ -439,8 +421,9 @@ export const processPortfolioEvents = async (
439421
portfolioKey, flowKey,
440422
() => startFlow(status, portfolioKey, flowKey, flowDetail),
441423
);
442-
return;
424+
break;
443425
}
426+
memory.snapshots.set(portfolioKey, { fingerprint, repeats: 0 });
444427
} catch (err) {
445428
const age = blockHeight - eventRecord.blockHeight;
446429
if (err.code === STALE_RESPONSE) {

services/ymax-planner/test/engine.test.ts

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,14 @@ const fakeSigningSmartWalletKit = async (
309309
['action', 'fee', 'memo', 'signerData']
310310
>[];
311311
const getBridgeSends = () => harden([...bridgeSends]);
312+
const mockDeliverTxResponseProto = {
313+
constructor: undefined,
314+
then: undefined,
315+
toJSON: undefined,
316+
valueOf: undefined,
317+
[Symbol.iterator]: undefined,
318+
[Symbol.toStringTag]: 'MockDeliverTxResponse',
319+
};
312320
const sendBridgeAction: SigningSmartWalletKit['sendBridgeAction'] = async (
313321
action,
314322
fee,
@@ -322,8 +330,9 @@ const fakeSigningSmartWalletKit = async (
322330
{ code: 0, height, transactionHash },
323331
{
324332
get: (target: object, key: string | symbol) => {
325-
if (key === 'then') return undefined;
326-
if (Object.hasOwn(target, key)) return target[key];
333+
for (const obj of [target, mockDeliverTxResponseProto]) {
334+
if (Object.hasOwn(obj, key)) return obj[key];
335+
}
327336
Fail`Not implemented: DeliverTxResponse ${q(key)}`;
328337
},
329338
},
@@ -466,6 +475,7 @@ const fakePortfolioKit = async ({
466475

467476
return {
468477
blockHeight: getBlockHeight(),
478+
portfoliosPathPrefix,
469479
portfolioId,
470480
portfolioPath,
471481
initialPortfolioStatus,
@@ -600,35 +610,47 @@ test.serial(
600610
);
601611

602612
test.serial('startFlow logs include traceId prefix', async t => {
603-
debugger;
604613
const kit = await fakePortfolioKit({
605614
accounts: { noble: AmountMath.make(depositBrand, 1_000_000n) },
606615
});
607616
const {
608617
blockHeight,
618+
portfoliosPathPrefix,
609619
portfolioId,
610620
portfolioPath,
611621
initialPortfolioStatus,
612622
powers,
613623
} = kit;
614624
const { updateVstorage } = kit.testPowers;
615625

616-
const flowId = 2;
617-
const portfolioStatus = harden({
618-
...initialPortfolioStatus,
626+
const fakeWithdrawFlow = (flowId: number, amountValue: bigint) => ({
619627
flowCount: 1,
620628
flowsRunning: {
621629
[`flow${flowId}`]: {
622630
type: 'withdraw',
623-
amount: AmountMath.make(depositBrand, 1_000_000n),
631+
amount: AmountMath.make(depositBrand, amountValue),
624632
},
625633
},
626634
});
627-
updateVstorage(portfolioPath, 'set', { object: portfolioStatus, wrap: true });
628635

629636
const portfolioKey = `portfolio${portfolioId}`;
630-
const flowKey = `flow${flowId}`;
631-
const tracePrefix = `[${portfolioKey}.${flowKey}] `;
637+
const portfolioStatus = harden({
638+
...initialPortfolioStatus,
639+
...fakeWithdrawFlow(4, 2_000_000n),
640+
});
641+
updateVstorage(portfolioPath, 'set', { object: portfolioStatus, wrap: true });
642+
643+
const portfolioId2 = portfolioId + 1;
644+
const portfolioKey2 = `portfolio${portfolioId2}`;
645+
const portfolioPath2 = `${portfoliosPathPrefix}.${portfolioKey2}`;
646+
const portfolioStatus2 = {
647+
...initialPortfolioStatus,
648+
...fakeWithdrawFlow(2, 1_000_000n),
649+
};
650+
updateVstorage(portfolioPath2, 'set', {
651+
object: portfolioStatus2,
652+
wrap: true,
653+
});
632654

633655
const captured: Array<{ level: 'debug' | 'info'; args: any[] }> = [];
634656
const originalLogTarget = console;
@@ -639,13 +661,11 @@ test.serial('startFlow logs include traceId prefix', async t => {
639661
info: (...args: any[]) => captured.push({ level: 'info', args }),
640662
});
641663

642-
const vstorageEventDetail = makeVstorageEventDetail(
643-
blockHeight,
644-
portfolioPath,
645-
portfolioStatus,
646-
);
647664
await processPortfolioEvents(
648-
[vstorageEventDetail],
665+
[
666+
makeVstorageEventDetail(blockHeight, portfolioPath, portfolioStatus),
667+
makeVstorageEventDetail(blockHeight, portfolioPath2, portfolioStatus2),
668+
],
649669
blockHeight,
650670
{ deferrals: [] },
651671
powers,
@@ -656,13 +676,13 @@ test.serial('startFlow logs include traceId prefix', async t => {
656676

657677
const tracedLogs = captured.filter(
658678
({ level, args }) =>
659-
['debug', 'info'].includes(level) && args[0] === tracePrefix,
660-
);
661-
t.true(tracedLogs.length >= 2, 'captured start and completion logs');
662-
t.true(
663-
tracedLogs.every(entry => entry.args[0] === tracePrefix),
664-
'all traced logs include the trace prefix',
679+
['debug', 'info'].includes(level) &&
680+
/\[portfolio[0-9]+[.]flow[0-9]+\] /.test(args[0]),
665681
);
682+
arrayIsLike(t, tracedLogs, [
683+
{ args: { 0: `[${portfolioKey}.flow4] `, 1: 'rejectPlan', length: 6 } },
684+
{ args: { 0: `[${portfolioKey2}.flow2] `, 1: 'resolvePlan', length: 6 } },
685+
]);
666686
});
667687

668688
/**

0 commit comments

Comments
 (0)