refactor(recovery): return RecoveryClaim from ClaimPendingTransactions - #1715
Conversation
|
Thanks a lot, @EvanYan1024 . I have left a few comments to the code to simplify it even further. I would also probe your opinion on introducing a new state for this transactions that are kind of orphaned. This would allows us to find them more easily and give them a second chance. |
Follow-up to review feedback on LFDT-Panurus#1715. The transactions table carried no information the recovery loop needed — the previous join existed only to recover stored_at, which already lives on the requests row. - common SQL: plain SELECT tx_id, stored_at FROM requests with the status + stored_at predicate; no join. - postgres atomic claim: collapse the CTE into a single UPDATE ... RETURNING (tx_id, stored_at). The previous outer SELECT existed only to recover stored_at via a second join against transactions; pulling it directly from RETURNING removes that hop. - recovery/manager.go: the dedupe-by-TxID block is gone. With the input now coming from requests (tx_id PK), each claim is unique by construction, so the seen-map was dead weight. Net -21 LoC across the three files; storage + recovery tests still green.
@adecaro to make sure I'm reading you right — is the proposal to change the SetStatus(ctx, txID, storage.Deleted, ...) call I added in #1708 (recovery/manager.go:331, on |
Yes, this is what I meant. We can introduce this new state in a new PR. That's also fine. |
Okay. I think that's fine. |
Follow-up to review feedback on LFDT-Panurus#1715. The transactions table carried no information the recovery loop needed — the previous join existed only to recover stored_at, which already lives on the requests row. - common SQL: plain SELECT tx_id, stored_at FROM requests with the status + stored_at predicate; no join. - postgres atomic claim: collapse the CTE into a single UPDATE ... RETURNING (tx_id, stored_at). The previous outer SELECT existed only to recover stored_at via a second join against transactions; pulling it directly from RETURNING removes that hop. - recovery/manager.go: the dedupe-by-TxID block is gone. With the input now coming from requests (tx_id PK), each claim is unique by construction, so the seen-map was dead weight. Net -21 LoC across the three files; storage + recovery tests still green.
a86414b to
c407c9e
Compare
|
Hi @EvanYan1024 , please, double check the DCO and the unit-test to see if it is a flaky test or something else. Thanks 🙏 |
Follow-up to review feedback on LFDT-Panurus#1715. The transactions table carried no information the recovery loop needed — the previous join existed only to recover stored_at, which already lives on the requests row. - common SQL: plain SELECT tx_id, stored_at FROM requests with the status + stored_at predicate; no join. - postgres atomic claim: collapse the CTE into a single UPDATE ... RETURNING (tx_id, stored_at). The previous outer SELECT existed only to recover stored_at via a second join against transactions; pulling it directly from RETURNING removes that hop. - recovery/manager.go: the dedupe-by-TxID block is gone. With the input now coming from requests (tx_id PK), each claim is unique by construction, so the seen-map was dead weight. Net -21 LoC across the three files; storage + recovery tests still green. Signed-off-by: Evan <evanyan@sign.global>
c407c9e to
791ad4d
Compare
adecaro
left a comment
There was a problem hiding this comment.
LGTM. Thanks @EvanYan1024 🙏
Follow-up to LFDT-Panurus#1708. The only consumer of ClaimPendingTransactions (recovery.Manager) reads just TxID and StoredAt; the other ten fields of TransactionRecord (action type, amounts, application/public metadata, status, ...) were always discarded by the caller. Introduce a dedicated RecoveryClaim {TxID, StoredAt} type in the driver layer and project only the two columns the recovery loop actually needs: - one fewer JOIN in the postgres CTE-based claim query - two JSON metadata unmarshals per row removed - eight unused columns dropped from the SELECT projection in both the common and postgres implementations The dedupe-by-TxID step in Manager.recoverTransactions is preserved because the underlying transactions table still produces one row per movement/output. Signed-off-by: Evan <evanyan@sign.global>
Follow-up to review feedback on LFDT-Panurus#1715. The transactions table carried no information the recovery loop needed — the previous join existed only to recover stored_at, which already lives on the requests row. - common SQL: plain SELECT tx_id, stored_at FROM requests with the status + stored_at predicate; no join. - postgres atomic claim: collapse the CTE into a single UPDATE ... RETURNING (tx_id, stored_at). The previous outer SELECT existed only to recover stored_at via a second join against transactions; pulling it directly from RETURNING removes that hop. - recovery/manager.go: the dedupe-by-TxID block is gone. With the input now coming from requests (tx_id PK), each claim is unique by construction, so the seen-map was dead weight. Net -21 LoC across the three files; storage + recovery tests still green. Signed-off-by: Evan <evanyan@sign.global>
791ad4d to
728b61b
Compare
Signed-off-by: Shashank <yshashank959@gmail.com> fix(multisig,boolpolicy): verify spend tx matches approved SpendRequest (#1691) Signed-off-by: SuyashAlphaC <suyashagrawal862@gmail.com> fsc v0.11.0 (#1702) Signed-off-by: Angelo De Caro <adc@zurich.ibm.com> feat(ttx): add versioned envelope for interactive protocol messages (#1700) Signed-off-by: SuyashAlphaC <suyashagrawal862@gmail.com> fix(recovery): unblock queue head by marking NotFound orphans Deleted after grace period (#1708) Signed-off-by: Evan <evanyan@sign.global> fix: cachedFetcher.update() no longer blocks token reads during DB refresh (#1535) Signed-off-by: Nitesh <nitesh@example.com> Signed-off-by: Nitesh Kumar <niteshkumar121411@gmail.com> Signed-off-by: NETIZEN-11 <kumarnitesh979875@gmail.com> perf(bulletproof): optimize IPA prover with batched MSMs (#1719) Signed-off-by: Ankit Basu <ankitbasu14@gmail.com> replace mutex with context aware semaphore (#1616) Signed-off-by: Hayim.Shaul@ibm.com <hayimsha@fhe3.haifa.ibm.com> refactor(recovery): return RecoveryClaim from ClaimPendingTransactions (#1715) Signed-off-by: Evan <evanyan@sign.global> Adding ZKP Benchmarking to test overhead of FSC nodes on TPS Signed-off-by: Effi-S <effi.szt@gmail.com> Signed-off-by: AKRAM@il.ibm.com <akram@akramb.vpc.cloud9.ibm.com> Fix #1635: Prevent audit lock starvation via defer pattern - Enhanced Audit() documentation requiring immediate defer Release() - Fixed integration test callers with proper error handling - Added unit tests for lock acquisition error handling - Leverages existing semaphore.Weighted for context-aware acquisition (PR #1616) - Ensures locks always released via defer, preventing DoS attacks Signed-off-by: AKRAM@il.ibm.com <akram@akramb.vpc.cloud9.ibm.com>
Follow-up to LFDT-Panurus#1715. TxStatus.Deleted previously overloaded two distinct outcomes: ledger-rejected / hash-mismatch txs (finality listener) and never-landed txs that the recovery loop promoted on NotFound past the grace period (added in LFDT-Panurus#1708). Operators could only tell the two apart by parsing the message column. Adds Orphan as a dedicated TxStatus value, re-exports it through the existing alias chains, and routes recovery/manager.go's grace-period path to it. Every other Deleted-checking call site (tokenlock cleanup, in-memory locker, checks.go, finality.go) is left unchanged: a retryable orphan still needs its input tokens locked, so status-based eager release would defeat the "second chance" intent. Orphan locks expire on the existing leaseExpiry clock instead. Signed-off-by: Evan <evanyan@sign.global>
Follow-up to LFDT-Panurus#1715. TxStatus.Deleted previously overloaded two distinct outcomes: ledger-rejected / hash-mismatch txs (finality listener) and never-landed txs that the recovery loop promoted on NotFound past the grace period (added in LFDT-Panurus#1708). Operators could only tell the two apart by parsing the message column. Adds Orphan as a dedicated TxStatus value, re-exports it through the existing alias chains, and routes recovery/manager.go's grace-period path to it. Every other Deleted-checking call site (tokenlock cleanup, in-memory locker, checks.go, finality.go) is left unchanged: a retryable orphan still needs its input tokens locked, so status-based eager release would defeat the "second chance" intent. Orphan locks expire on the existing leaseExpiry clock instead. Signed-off-by: Evan <evanyan@sign.global>
Per @adecaro's review on LFDT-Panurus#1722, update the recovery-related documentation for changes in this PR (LFDT-Panurus#1722) and the previous recovery PRs (LFDT-Panurus#1708, LFDT-Panurus#1715). - docs/configuration.md: add notFoundGracePeriod to the recovery config sample, default-values list, and Parameter Relationships section. - docs/services/storage.md: rewrite the Recovery Process workflow to cover the lightweight RecoveryClaim projection (LFDT-Panurus#1715), the atomic PostgreSQL claim, and the new NotFound -> Orphan promotion branch. Also extend the Requests-table status enumeration with Orphan. - docs/services/recovery.md: add SetStatus to the Storage interface description, refresh the config and Go usage examples with NotFoundGracePeriod, add an Orphan step to the Recovery Process Flow, add an Orphan entry to Error Handling, and add a new Transaction Status Lifecycle section covering Pending / Confirmed / Deleted / Orphan and how the claim-query filter excludes terminal statuses. Signed-off-by: Evan <evanyan@sign.global>
Closes #1714
Follow-up to #1708.
Summary
ClaimPendingTransactionspreviously returned[]*TransactionRecord, but the only consumer (recovery.Manager) reads justTxIDandStoredAt— the other ten fields (action type, amounts, application/public metadata, status, ...) were always discarded.Introduce a dedicated lightweight type in the driver layer, mirroring the existing
RecoveryClaimParamsinput type:…and project only the two columns the recovery loop actually needs from SQL.
Concrete savings
transactionsJOIN. Both the postgres atomic claim and the common permissive query now readtx_id, stored_atdirectly fromrequests. The postgres CTE collapses to a singleUPDATE ... RETURNING (tx_id, stored_at); the previous outerSELECTexisted only to recoverstored_atvia a second join.application_metadata,public_metadata).SELECTprojection (action_type,sender_eid,recipient_eid,token_type,amount,status,application_metadata,public_metadata).pendingTxstruct inrecovery.Managerremoved —*RecoveryClaimis now used directly as the work-channel element, eliminating a redundant intermediate type.TxIDblock inManager.recoverTransactionsremoved (see "Semantic change" below).Semantic change — please confirm
olderThanis now compared againstrequests.stored_at(system insert time) instead oftransactions.stored_at(the caller-suppliedTransactionRecord.Timestamp).Rationale: the recovery loop's notion of "how long has this row been stuck in pending" — which
NotFoundGracePeriodin #1708 already relies on — is a system-time question, not a business-time one. The previous JOIN againsttransactionswas using a caller-controlled field whose meaning depends on whoever callsAddTransaction, making the recovery clock implicitly dependent on the producer's choice ofTimestamp. Anchoring onrequests.stored_atmakes the filter robust against that.In typical usage
AddTokenRequestandAddTransactionare committed in the same SQL transaction, so the two columns are within milliseconds of each other and observable behaviour is identical. The change matters only when a caller deliberately writes a backdatedTransactionRecord.Timestamp— which the recovery loop should arguably not depend on.Flagging this explicitly because it goes a bit beyond the literal description in #1714. Happy to revert this specific point if @adecaro / maintainers prefer to keep the historical behaviour.
Dedupe block removed
Manager.recoverTransactionsused to dedupe byTxIDbecause the previous SQL joined againsttransactions(which fans out to one row per movement/output for a single tx_id). The new query reads fromrequestswheretx_idis the primary key, so each returned claim is unique by construction and the seen-map became dead weight.Scope (5 layers)
db/driver/{ttx,audit}.go— newRecoveryClaimtype + interface signature.db/sql/common/transactions.go— query readstx_id, stored_atfromrequestsonly; no join.db/sql/postgres/transactions.go— CTE collapses toUPDATE ... RETURNING; deadunmarshalMetadata+encoding/jsonimport removed.{ttxdb,auditdb}/store.go—StoreServicesignature + re-exported type alias.network/fabric/network.go+recovery/manager.go— adapter andrecovery.Storageinterface signatures;pendingTxand the dedupe-by-TxIDblock deleted,RecoveryClaimconsumed directly by the worker goroutines.Mocks regenerated via
counterfeiter v6.12.2(recovery/mock/storage.go,auditor/mock/audit_transaction_store.go).Behaviour preserved
NotFoundGracePeriodorphan-promotion logic from fix(recovery): unblock queue head by marking NotFound orphans Deleted after grace period #1708 is unchanged (and now lines up witholderThansemantically — both are system-time anchored).Test plan
go build ./...go vet ./...go test ./token/services/storage/recovery/...go test ./token/services/storage/...— sqlite, ttxdb, auditdb, tokendb, walletdb all greengo test ./token/services/auditor/... ./token/services/network/fabric/...make testing-docker-images && go test -race ./token/services/storage/db/sql/postgres -count=1— full postgres suite green (this is the path that was red on the first CI run; tests now seedrequests.stored_atvia a newageRequestshelper so the filter actually matches)