feat(backend): transactional escrow orchestration service (#21) - #36
Conversation
…bs#21) Submits and confirms escrow-contract operations over Soroban RPC with idempotency keys, exactly-once submission semantics, retry/backoff, and reconciliation against the on-chain event ingestion pipeline from workman-labs#22. Transaction envelopes are relayed as opaque signed XDR rather than built or decoded here -- no verifiable Java/Soroban SDK exists on Maven Central to do that safely; see backend-api/docs/ESCROW_ORCHESTRATION.md for the full write-up of that and the other design decisions. Also fixes a pre-existing test-suite flake where @SpringBootTest classes that don't disable scheduling leave background pollers running against the shared test database for the rest of the JVM's life, racing with whatever test runs next.
|
Thanks — this is a very thorough implementation and I appreciate the attention to idempotency, the reconciliation model, and the end-to-end tests. A few requests / suggestions before merging:
Minor
|
) - Retry/backoff is now config-driven (EscrowOrchestrationRetryProperties: max-attempts, base-delay, max-delay, jitter) instead of a hardcoded constant, with +/- jitter to avoid a thundering herd on retry. - SorobanRpcClient now actually applies soroban.rpc.request-timeout to the underlying OkHttp calls (was previously configured but unused), and correlates every JSON-RPC call with a request id, truncating logged/ exception-embedded bodies. - POST /api/v1/escrow/orchestrations signals idempotent-replay via an X-Idempotent-Replay response header (submit() now returns a SubmitOutcome distinguishing created vs. replayed) instead of overloading status codes. - Structured INFO/WARN logs for every state transition (created, submitted, confirmed, on-chain failure, retry scheduled, DEAD_LETTER, reconciliation mismatch). - operationRef now rejects a literal '"' (closes a theoretical topic-match injection in EscrowReconciliationService's LIKE query). - Expanded docs/ESCROW_ORCHESTRATION.md: migration/schema strategy, concurrency/locking rationale, retry & observability, API behavior, and an Operations section covering reconciliation-window tuning and recovery steps for ingestion-pipeline lag. - Added Javadoc to EscrowOrchestrationInserter and EscrowOrchestrationRequestRepository#claimNext explaining the REQUIRES_NEW + pessimistic-lock choice over optimistic CAS, and expected behavior under lock contention.
|
Thanks for the thorough review — pushed a follow-up commit (47bd781) addressing everything actionable, and replying inline on the rest below. 1) Migrations/schema. No migration file — this repo has no Flyway/Liquibase; schema is managed entirely via 2) Concurrency & locking. Added Javadoc to 3) Retry/backoff & observability.
4) Soroban RPC client safety. Every call now gets a correlation id (also sent as the JSON-RPC 5) Reconciliation dependencies & window. Added an "Operations" section to the docs covering exactly this: how to tune 6) Tests & scheduler leak. The 7) Error surface & API behavior. 8) CI trigger. The Minor (inline comments). Added comments on the idempotency-insertion path ( |
meshackyaro
left a comment
There was a problem hiding this comment.
Here are a few suggestions/improvements
- backend-api/src/main/java/com/guildworkman/api/escrow/service/EscrowOrchestrationService.java — deadLetter() (end of file): deadLetter sets status and lastError but does not persist the entity. Please either call repository.save(entity) inside deadLetter, or document that every caller must save afterwards. Persisting here is safer to avoid future omissions; also consider clearing nextAttemptAt or otherwise marking the entity terminal.
- backend-api/src/main/java/com/guildworkman/api/escrow/rpc/SorobanRpcClient.java — call(...) and exception paths: you truncate response bodies but not the request body. Request bodies (signed XDR) can be large and sensitive; do not embed the full request XDR in exception messages or logs. Please redact or truncate the request XDR before logging or including it in exceptions (e.g., show first N chars + total length), or omit it entirely and rely on rpcId for tracing.
- backend-api/src/main/java/com/guildworkman/api/escrow/api/SubmitOrchestrationRequest.java — signedTransactionXdr DTO: add an explicit maximum size validation (@SiZe(max = ...), e.g. 8192) to prevent very large payloads from being accepted (DoS/vector). Optionally add a base64 pattern validator if desired.
- backend-api/src/main/java/com/guildworkman/api/chain/repository/OnChainEventRepository.java — findByContractIdAndTopicsContaining(...): note that this uses LIKE on a JSON-encoded topics column; substring LIKE scans can be expensive for large event volumes. Consider documenting indexing guidance, or adding a computed column / normalized topics table if volumes increase.
- backend-api/pom.xml — surefire systemPropertyVariables comment: add a one-line note to the top-level README or CI docs explaining this is to avoid flaky scheduler leakage across @SpringBootTest contexts, so future contributors understand why these system properties exist.
- backend-api/src/main/resources/application.properties — schema management note: docs mention ddl-auto=update. Please add a short comment in application.properties (or docs/ESCROW_ORCHESTRATION.md) reminding operators that Hibernate ddl-auto=update is used and that production should consider an explicit migration strategy (Flyway/Liquibase) for controlled schema changes.
- Tests: add a unit test asserting SorobanRpcClient does not leak full request XDR in exception messages (sanitization), and add a DTO validation test for signedTransactionXdr size if you add the @SiZe constraint.
Small improvements (non-blocking, recommended)
Consider persisting or incrementally exposing metrics (Micrometer counters/gauges) for submissions, retries, dead-letters, and reconciliation mismatches — logs are good, but metrics + alerts make ops easier. This is optional and can be a separate follow-up PR.
Consider adding a small admin endpoint or documented SQL snippet to requeue a MISMATCHED request (docs already mention this SQL; adding a small admin API could help operators).
Consider a circuit breaker/rate-limiter around SorobanRpcClient to avoid amplified retries if the RPC endpoint is unhealthy.
…rkman-labs#21) - Real bug fix: deadLetter() didn't persist the entity itself, relying on every caller to remember to save afterward. Now saves internally and callers no longer double-save. - signedTransactionXdr gets explicit size (8192) and base64-pattern validation at the API boundary. - SorobanRpcClient safety: added tests asserting a large signed XDR never appears untruncated in exception messages across the HTTP-error, JSON-RPC-error, and IOException paths (including a response that echoes the request back). - New admin-only POST /api/v1/escrow/orchestrations/{id}/requeue-reconciliation, wrapping the manual SQL the docs already described, so a MISMATCHED request can be reset to PENDING once the missing on-chain event is confirmed to exist. - Doc/comment additions: LIKE-scan cost and indexing guidance on OnChainEventRepository#findByContractIdAndTopicsContaining, a ddl-auto reminder in application.properties, a README note on why the surefire scheduler-delay overrides exist, and a documented (not implemented) decision on why no circuit breaker was added around SorobanRpcClient.
|
Thanks — pushed a follow-up commit (0acd4a5) with fixes/additions for all of these.
Request-XDR leakage in
Surefire comment → contributor docs. Added a "Background
Tests. Added the sanitization tests and the DTO validation tests as requested (see above). Non-blocking suggestions:
Local suite is green — 106/106, run 3x ( |
meshackyaro
left a comment
There was a problem hiding this comment.
Fantastic work — this is a well-designed, well-tested feature and a real engineering-quality contribution.
Highlights I appreciated
- Clear scope and reasoning: the PR description and ESCROW_ORCHESTRATION.md explain the architecture decisions (especially the choice to treat XDR as opaque and avoid hand-rolling an SDK), which makes the tradeoffs easy to understand.
Robust correctness focus: idempotency via a DB unique constraint + REQUIRES_NEW guard, exactly-once submission semantics, and the separate submit/poll cycles show careful thought about concurrency and failure modes. - Excellent test coverage: unit tests for the service and client, MockWebServer for RPC, and a comprehensive integration test that covers idempotency under concurrency, submit→confirm lifecycle, backoff→dead-letter, and reconciliation scenarios. Tests like these make me confident this will behave correctly in production.
- Operationally minded: reconciliation against the ingested on-chain event stream, a configurable grace window for MISMATCHED detection, and a DEAD_LETTER terminal state are practical choices for real-world operation and recovery.
- Nice fix for a long-standing flake: the scheduler-leak/test-interference fix shows attention to CI stability — that kind of polish matters.
Packaging / build: mvnw verify passes and the new code is organized and documented; that makes review and adoption easier.
This PR is high quality and ready to land from a design, test, and documentation perspective — great job carrying this through end-to-end, @kris-nana
Summary
Closes #21.
Implements a backend orchestration service that submits and confirms escrow-contract operations over Soroban RPC, with idempotency keys, exactly-once submission semantics, retry/backoff, and reconciliation of on-chain versus off-chain state.
EscrowOrchestrationService— idempotent submission (idempotency_keyunique constraint +REQUIRES_NEWinsert guard, mirroringChainEventInserterfrom On-Chain Event Ingestion Pipeline with Transactional Outbox & Replay #22), two independent scheduled claim/poll cycles (submitPending→SUBMITTED,pollSubmitted→CONFIRMED/FAILED), capped exponential backoff with aDEAD_LETTERterminal state.SorobanRpcClient— thin JSON-RPC 2.0 client (sendTransaction/getTransaction) built on the existingOkHttpClientbean. Transaction envelopes are relayed as opaque, already-signed base64 XDR — this service does not build or decode XDR itself.EscrowReconciliationService— flags aCONFIRMEDrequestMISMATCHEDif no corroboratingPROCESSEDon-chain event (from the On-Chain Event Ingestion Pipeline with Transactional Outbox & Replay #22 ingestion pipeline) appears within a configurable grace window.POST /api/v1/escrow/orchestrations(submit, idempotent) andGET /api/v1/escrow/orchestrations/{id}(status), both behind bearer auth.Architecture decision, documented in
backend-api/docs/ESCROW_ORCHESTRATION.md: no official Stellar/Soroban SDK is published to Maven Central for Java (checkedorg.stellar,network.stellar,java-stellar-sdk,stellar-android-sdk), so this PR deliberately avoids hand-rolling XDR encoding. Submission/confirmation only ever pass opaque strings through Soroban RPC (which is allsendTransaction/getTransactionneed), and reconciliation reuses the already-ingested on-chain event stream from #22 instead of issuing rawgetLedgerEntriesreads.Also fixes a pre-existing test-suite flake:
@SpringBootTestclasses that don't disable scheduling leave background@Scheduledpollers running against the shared test database for the rest of the JVM's life (Spring caches contexts), racing with whatever test runs next and mutating its rows out from under it — this was intermittently failingChainEventServiceIntegrationTeston CI already (see the #22 PR's run history) and now hit the new escrow integration test too. Fixed viamaven-surefire-pluginsystemPropertyVariablesdefaulting all poller delays to 1h; explicit per-test@SpringBootTest(properties = ...)overrides still take precedence.Test plan
./mvnw -B test— 93/93 passing locally against Postgres 16, run 3x to confirm the scheduler-leak fix holds./mvnw -B verify— builds and packages cleanlyEscrowOrchestrationServiceTest,EscrowReconciliationServiceTest,SorobanRpcClientTest(MockWebServer)EscrowOrchestrationIntegrationTest— idempotency (incl. concurrent duplicate submits), full submit→confirm lifecycle, RPC failure → backoff → dead-letter, pessimistic-lock double-submission guard, reconciliation (matched/mismatched/pending-within-window)Testworkflow (.github/workflows/test.yml) runs onbackend-api/**changes — pending on this PR