Skip to content

Commit a258868

Browse files
fix: no such transaction ID
Signed-off-by: Thibaud Germain <thibaud.germain1@ibm.com>
1 parent 05e391a commit a258868

2 files changed

Lines changed: 63 additions & 3 deletions

File tree

token/services/storage/services/recovery/manager.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,9 +437,10 @@ func (m *Manager) recoverTransaction(ctx context.Context, txID string, storedAt
437437
// "rpc error: code = NotFound desc = transaction ID [X]: not found in
438438
// index: tx not found"):
439439
//
440-
// - "code = NotFound" — raw gRPC status text
441-
// - "not found in index" — committer's gRPC status desc field
442-
// - "tx not found" — FSC finality.TxNotFound sentinel appended
440+
// - "code = NotFound" — raw gRPC status text
441+
// - "not found in index" — committer's gRPC status desc field
442+
// - "tx not found" — FSC finality.TxNotFound sentinel appended
443+
// - "no such transaction ID" — direct return from fabric in common/ledger/blkstorage/blockindex.go
443444
// by fabric-x ledger.GetTransactionByID
444445
// (fabric-smart-client/platform/fabricx/core/ledger/ledger.go:64).
445446
// Stable across committer error format changes since the sentinel
@@ -456,6 +457,8 @@ func isNotFoundError(err error) bool {
456457
return true
457458
case strings.Contains(msg, "tx not found"):
458459
return true
460+
case strings.Contains(msg, "no such transaction ID"):
461+
return true
459462
}
460463

461464
return false

token/services/storage/services/recovery/manager_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,63 @@ func TestManager_StopDuringFanOutDoesNotDeadlock(t *testing.T) {
408408
require.NoError(t, manager.Stop())
409409
}
410410

411+
// TestManager_PromoteOrphanOnNoSuchTransactionID verifies that the
412+
// "no such transaction ID" error message returned by the Fabric-X ledger
413+
// (fabric-smart-client/platform/fabricx/core/ledger/ledger.go) is recognised
414+
// as a NotFound error, so the manager promotes the tx to storage.Orphan after
415+
// the NotFoundGracePeriod has elapsed.
416+
func TestManager_PromoteOrphanOnNoSuchTransactionID(t *testing.T) {
417+
logger := logging.MustGetLogger()
418+
mockDB := &mock2.Storage{}
419+
mockHandler := &mock2.Handler{}
420+
config := recovery2.Config{
421+
Enabled: true,
422+
TTL: 100 * time.Millisecond,
423+
ScanInterval: 100 * time.Millisecond,
424+
BatchSize: 100,
425+
WorkerCount: 1,
426+
LeaseDuration: time.Second,
427+
AdvisoryLockID: 1,
428+
InstanceID: "test-instance",
429+
NotFoundGracePeriod: 10 * time.Millisecond,
430+
}
431+
432+
// stored_at well beyond the 10ms grace period so the promotion fires.
433+
txRecord := &ttxdb.RecoveryClaim{
434+
TxID: "txNoSuchTx",
435+
StoredAt: time.Now().Add(-time.Hour),
436+
}
437+
438+
leadership1 := &mock2.Leadership{}
439+
leadership1.CloseReturns(nil)
440+
leadership2 := &mock2.Leadership{}
441+
leadership2.CloseReturns(nil)
442+
443+
mockDB.AcquireRecoveryLeadershipReturnsOnCall(0, leadership1, true, nil)
444+
mockDB.AcquireRecoveryLeadershipReturns(leadership2, true, nil)
445+
mockDB.ClaimPendingTransactionsReturnsOnCall(0, []*ttxdb.RecoveryClaim{txRecord}, nil)
446+
mockDB.ClaimPendingTransactionsReturns([]*ttxdb.RecoveryClaim{}, nil)
447+
// Use the Fabric-X ledger sentinel substring directly.
448+
mockHandler.RecoverReturns(errors.New("Failed to get transaction with id d48c4, error no such transaction ID [d48c] in index"))
449+
mockDB.ReleaseRecoveryClaimReturns(nil)
450+
mockDB.SetStatusReturns(nil)
451+
452+
manager := recovery2.NewManager(logger, mockDB, mockHandler, config)
453+
454+
require.NoError(t, manager.Start())
455+
// Wait for the initial sweep (jitter up to 1s + handler invocation).
456+
time.Sleep(1300 * time.Millisecond)
457+
_ = manager.Stop()
458+
459+
require.GreaterOrEqual(t, mockHandler.RecoverCallCount(), 1)
460+
require.Equal(t, 1, mockDB.SetStatusCallCount(), "expected exactly one SetStatus call for the orphan promotion")
461+
462+
_, gotTxID, gotStatus, gotMsg := mockDB.SetStatusArgsForCall(0)
463+
assert.Equal(t, "txNoSuchTx", gotTxID)
464+
assert.Equal(t, storage.Orphan, gotStatus, "orphan path must promote to storage.Orphan, not storage.Deleted")
465+
assert.Contains(t, gotMsg, "tx never reached ledger")
466+
}
467+
411468
func TestDefaultConfig(t *testing.T) {
412469
config := recovery2.DefaultConfig()
413470

0 commit comments

Comments
 (0)