Skip to content

refactor(recovery): return RecoveryClaim from ClaimPendingTransactions - #1715

Merged
adecaro merged 2 commits into
LFDT-Panurus:mainfrom
Built-by-Sign:recovery-claim-pending-trim-return
May 20, 2026
Merged

refactor(recovery): return RecoveryClaim from ClaimPendingTransactions#1715
adecaro merged 2 commits into
LFDT-Panurus:mainfrom
Built-by-Sign:recovery-claim-pending-trim-return

Conversation

@EvanYan1024

@EvanYan1024 EvanYan1024 commented May 18, 2026

Copy link
Copy Markdown
Contributor

Closes #1714

Follow-up to #1708.

Summary

ClaimPendingTransactions previously returned []*TransactionRecord, but the only consumer (recovery.Manager) reads just TxID and StoredAt — 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 RecoveryClaimParams input type:

type RecoveryClaim struct {
    TxID     string
    StoredAt time.Time
}

…and project only the two columns the recovery loop actually needs from SQL.

Concrete savings

  • No more transactions JOIN. Both the postgres atomic claim and the common permissive query now read tx_id, stored_at directly from requests. The postgres CTE collapses to a single UPDATE ... RETURNING (tx_id, stored_at); the previous outer SELECT existed only to recover stored_at via a second join.
  • Two JSON unmarshals per row removed (application_metadata, public_metadata).
  • Eight unused columns dropped from the SELECT projection (action_type, sender_eid, recipient_eid, token_type, amount, status, application_metadata, public_metadata).
  • Internal pendingTx struct in recovery.Manager removed*RecoveryClaim is now used directly as the work-channel element, eliminating a redundant intermediate type.
  • Dedupe-by-TxID block in Manager.recoverTransactions removed (see "Semantic change" below).

Semantic change — please confirm

olderThan is now compared against requests.stored_at (system insert time) instead of transactions.stored_at (the caller-supplied TransactionRecord.Timestamp).

Rationale: the recovery loop's notion of "how long has this row been stuck in pending" — which NotFoundGracePeriod in #1708 already relies on — is a system-time question, not a business-time one. The previous JOIN against transactions was using a caller-controlled field whose meaning depends on whoever calls AddTransaction, making the recovery clock implicitly dependent on the producer's choice of Timestamp. Anchoring on requests.stored_at makes the filter robust against that.

In typical usage AddTokenRequest and AddTransaction are 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 backdated TransactionRecord.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.recoverTransactions used to dedupe by TxID because the previous SQL joined against transactions (which fans out to one row per movement/output for a single tx_id). The new query reads from requests where tx_id is the primary key, so each returned claim is unique by construction and the seen-map became dead weight.

Scope (5 layers)

  1. db/driver/{ttx,audit}.go — new RecoveryClaim type + interface signature.
  2. db/sql/common/transactions.go — query reads tx_id, stored_at from requests only; no join.
  3. db/sql/postgres/transactions.go — CTE collapses to UPDATE ... RETURNING; dead unmarshalMetadata + encoding/json import removed.
  4. {ttxdb,auditdb}/store.goStoreService signature + re-exported type alias.
  5. network/fabric/network.go + recovery/manager.go — adapter and recovery.Storage interface signatures; pendingTx and the dedupe-by-TxID block deleted, RecoveryClaim consumed directly by the worker goroutines.

Mocks regenerated via counterfeiter v6.12.2 (recovery/mock/storage.go, auditor/mock/audit_transaction_store.go).

Behaviour preserved

Test plan

  • go build ./...
  • go vet ./...
  • go test ./token/services/storage/recovery/...
  • go test ./token/services/storage/... — sqlite, ttxdb, auditdb, tokendb, walletdb all green
  • go 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 seed requests.stored_at via a new ageRequests helper so the filter actually matches)
  • CI green

@adecaro
adecaro self-requested a review May 18, 2026 09:34
@adecaro adecaro self-assigned this May 18, 2026
@adecaro adecaro added this to the Q2/26 milestone May 18, 2026
Comment thread token/services/storage/db/sql/common/transactions.go Outdated
Comment thread token/services/storage/recovery/manager.go Outdated
@adecaro

adecaro commented May 18, 2026

Copy link
Copy Markdown
Contributor

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.
What do you think?

EvanYan1024 added a commit to Built-by-Sign/fabric-token-sdk that referenced this pull request May 18, 2026
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.
@EvanYan1024

Copy link
Copy Markdown
Contributor Author

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.

@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
the NotFoundGracePeriod path) so it sets a new dedicated Orphan state instead? That way Deleted stays reserved for explicit operator/user deletion, and "tried to
commit, never landed on chain within the grace window" gets its own unambiguous signal.

@adecaro

adecaro commented May 19, 2026

Copy link
Copy Markdown
Contributor

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.

@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 the NotFoundGracePeriod path) so it sets a new dedicated Orphan state instead? That way Deleted stays reserved for explicit operator/user deletion, and "tried to commit, never landed on chain within the grace window" gets its own unambiguous signal.

Yes, this is what I meant. We can introduce this new state in a new PR. That's also fine.

@EvanYan1024

Copy link
Copy Markdown
Contributor Author

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.

@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 the NotFoundGracePeriod path) so it sets a new dedicated Orphan state instead? That way Deleted stays reserved for explicit operator/user deletion, and "tried to commit, never landed on chain within the grace window" gets its own unambiguous signal.

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.

EvanYan1024 added a commit to Built-by-Sign/fabric-token-sdk that referenced this pull request May 19, 2026
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.
@EvanYan1024
EvanYan1024 force-pushed the recovery-claim-pending-trim-return branch from a86414b to c407c9e Compare May 19, 2026 13:03
@adecaro

adecaro commented May 19, 2026

Copy link
Copy Markdown
Contributor

Hi @EvanYan1024 , please, double check the DCO and the unit-test to see if it is a flaky test or something else. Thanks 🙏

EvanYan1024 added a commit to Built-by-Sign/fabric-token-sdk that referenced this pull request May 19, 2026
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>
@EvanYan1024
EvanYan1024 force-pushed the recovery-claim-pending-trim-return branch from c407c9e to 791ad4d Compare May 19, 2026 15:19
@adecaro
adecaro self-requested a review May 20, 2026 04:24

@adecaro adecaro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@adecaro
adecaro force-pushed the recovery-claim-pending-trim-return branch from 791ad4d to 728b61b Compare May 20, 2026 04:30
@adecaro
adecaro merged commit 520c80f into LFDT-Panurus:main May 20, 2026
94 checks passed
@EvanYan1024
EvanYan1024 deleted the recovery-claim-pending-trim-return branch May 20, 2026 06:46
AkramBitar pushed a commit that referenced this pull request May 20, 2026
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>
EvanYan1024 added a commit to EvanYan1024/fabric-token-sdk that referenced this pull request May 21, 2026
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>
EvanYan1024 added a commit to EvanYan1024/fabric-token-sdk that referenced this pull request May 21, 2026
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>
EvanYan1024 added a commit to EvanYan1024/fabric-token-sdk that referenced this pull request May 21, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

recovery: ClaimPendingTransactions over-projects rows the caller never reads

2 participants