Skip to content

Retry transaction status transitions on write conflicts + Zod validation pipe acceptance (Closes #412, Closes #418) - #473

Merged
Cjay-Cyber-2 merged 1 commit into
ASTROIDX556:mainfrom
chiprime:implement-zod-validation
Oct 22, 2026
Merged

Cjay-Cyber-2 merged 1 commit into
ASTROIDX556:mainfrom
chiprime:implement-zod-validation

Conversation

@chiprime

Copy link
Copy Markdown
Contributor

Summary

This PR delivers the remaining work for two backend issues in one change set: the Prisma transaction-retry integration (#412) and the Zod request-validation pipe (#418).

Closes #412
Closes #418


Issue #412 — Refactor Prisma transaction retry wrapper for concurrent Soroban transaction status updates

Problem: High-throughput agent operations race on transaction status rows — fee-bump retries, approvals landing mid-execute, and concurrent cancel calls all did a read → check → write sequence with no atomicity. Two workers could both observe DRAFT, and the loser's write silently clobbered the winner's: a cancellation could overwrite a transaction already submitted on-chain, and two executes could both pass the status gate before either marked the row SUBMITTED.

Solution: the reusable retry utility from src/database/prisma-transaction.service.ts (runWithRetry, P2034-aware, exponential backoff) is now integrated into the transactions module where multi-step status updates occur.

What changed

  • src/modules/transactions/transaction.repository.ts
    • findById() and update() accept an optional transactional client (PrismaService | Prisma.TransactionClient, defaulting to the shared PrismaService), so a status check and its write commit — or roll back — together. Same pattern as BudgetRepository.
  • src/modules/transactions/transaction.service.ts
    • Injects PrismaTransactionService (provided by the global DatabaseModule — no module wiring changes needed).
    • execute(): the DRAFT/PENDING → SUBMITTED transition is now a guarded read-check-write executed inside runWithRetry at SERIALIZABLE isolation. A serialization conflict (P2034) is replayed with exponential backoff; the replay re-reads the row, sees SUBMITTED, and throws ConflictException before any funds move on-chain — no double submission.
    • cancel(): same guarded pattern for DRAFT/PENDING → CANCELLED, so a cancel can never overwrite a transaction a concurrent worker just submitted or completed. NotFoundException / ConflictException semantics are unchanged and, being non-conflict errors, are never replayed.
    • Executed/cancellable status sets extracted to named constants (EXECUTED_STATUSES, CANCELLABLE_STATUSES) shared by the fast-path check and the in-transaction guard.
  • src/modules/transactions/tests/transaction.service.spec.ts — updated construction sites for the new constructor dependency (passthrough transaction manager, same pattern as budget.service-lock.spec.ts).
  • src/modules/transactions/tests/transaction-status-retry.spec.ts (new) — unit tests that simulate concurrent database conflicts against a real PrismaTransactionService whose $transaction aborts the first N attempts with P2034:
    • conflict replayed → cancel succeeds on the retry, writes committed exactly once;
    • status check + write proven to run inside the retried client at Serializable isolation;
    • replay observes a concurrently committed SUBMITTED row → ConflictException, no write, no event emitted;
    • non-conflict DB error propagates after a single attempt (no replay);
    • missing row → NotFoundException without retrying;
    • retry budget exhausted → P2034 surfaces after the default 3 attempts;
    • execute(): conflict replayed → submits on-chain exactly once; lost race → ConflictException and stellar.submitPayment never called; already-executed row fast-fails without opening a transaction.

Acceptance criteria

  • Reusable Prisma transaction helper in src/database/ with configurable exponential retry for transient conflicts (runWithRetry, attempts/backoff configurable, P2034)
  • Integrated into the transactions module's multi-step status update paths
  • Unit tests simulating concurrent conflicts and verifying successful retry execution
  • npm run db:verify scripts unaffected (no migrations touched)

Issue #418 — Implement Zod Validation Pipe for Request Body Deserialization across Domain Controllers

State: the acceptance criteria for this issue are satisfied in main by the cumulative work of earlier merged PRs (#166, #192, #274, #353, #378). This PR verifies the criteria end-to-end against the current branch and keeps them intact; no pipe re-implementation was required.

Acceptance criteria verification

  • ZodValidationPipe in src/common/pipes/zod-validation.pipe.ts implementing PipeTransform — exists, generic over ZodType<T>, parses via .safeParse() (never throws unhandled), supports custom error maps, per-path custom messages and locale options; strict TypeScript, no any.
  • Validation errors return a clean array of path-and-message objects matching the standard error envelope — failures throw ZodValidationException extends BadRequestException carrying { code: 'VALIDATION_ERROR', message, details: [{ path, message }] }, which the global exception filter renders as the standard 400 problem-details envelope ({ success: false, error: ... }).
  • Applied to at least two key module controllers (wallets and spending policies) as reference implementations — src/modules/wallets/wallet.controller.ts (createWalletSchema, updateWalletSchema, pagination query) and src/modules/policies/policy.controller.ts (createPolicySchema, updatePolicySchema, simulatePolicySchema, pagination query); additionally applied across auth, agents, transactions, budgets, approvals, memory, webhooks, organizations, notifications, risk, audit, admin and developer controllers, documented in docs/validation.md.
  • Unit tests in src/common/pipes/zod-validation.pipe.spec.ts — cover valid payloads (incl. defaults/coercion/stripping), missing required fields, type mismatches, nested/array paths, custom messages and error maps; plus zod-validation.pipe.integration.spec.ts and problem-details.integration.spec.ts covering the { success: false, error: ... } / HTTP 400 envelope end-to-end.

Acceptance criteria

  • ZodValidationPipe created with structured path/message details
  • Standard error envelope on invalid payloads (HTTP 400)
  • Reference controllers: wallets + spending policies
  • Unit tests for valid payloads, missing required fields and type mismatches

Validation

Check Command Result
Tests npm run test ✅ 183 files, 1904 tests passed (exit 0)
Typecheck npm run typecheck ✅ clean (exit 0)
Lint npm run lint ✅ clean (exit 0)
  • Conflicts: branch is based on the current upstream/main (571a451); re-fetched immediately before push — 0 commits behind, no merge conflicts.
  • No database migrations were added or modified (npm run db:verify unaffected).

…conflicts

Concurrent workers (fee-bump retries, approvals landing mid-execute,
racing cancels) could clobber each other's transaction status writes.
The DRAFT/PENDING -> SUBMITTED and DRAFT/PENDING -> CANCELLED
transitions now run as SERIALIZABLE units of work through
PrismaTransactionService.runWithRetry: a serialization conflict (P2034)
is replayed with exponential backoff, and the replay re-reads the row
so a lost race surfaces a ConflictException before any funds move
on-chain. Repository reads/writes accept the transactional client so
check and update commit or roll back together.

Closes the transactions-module integration gap for ASTROIDX556#412.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@Cjay-Cyber-2
Cjay-Cyber-2 merged commit 930cb81 into ASTROIDX556:main Oct 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants