Skip to content

Commit c4a2f1f

Browse files
atharrva01AkramBitar
authored andcommitted
fix(recovery): address review, and fix cleanup the same way
Keystore cleanup had the same bug, so it is fixed here rather than left for a follow-up: the id now comes from the token SKI cleanup table name and AcquireCleanupLeadership drops its lockID param, same as recovery. Also from review: - deleted NewAdvisoryLockFactory and NewCleanupLeaderFactory, both had no production callers left and keeping them kept the caller-supplied lock id around, which is the footgun being removed - the lock id tests now call the production recoveryLockID instead of mirroring the derivation, so they fail if it changes - added a test that LoadConfig still succeeds with a stale advisoryLockID key - dropped the cleanup advisoryLockID from docs/configuration.md Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
1 parent f796a61 commit c4a2f1f

15 files changed

Lines changed: 111 additions & 86 deletions

File tree

docs/configuration.md

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -285,14 +285,6 @@ token:
285285
# Performance impact: More workers increase CPU utilization during cleanup sweeps.
286286
workerCount: 1
287287

288-
# advisoryLockID is the PostgreSQL advisory lock identifier used for cleanup leader election.
289-
# This ensures only one replica performs cleanup sweeps at a time in multi-instance deployments.
290-
# Default: 8389190333894887277 (hex: 0x74746b636c65616e, ASCII: "ttkclean")
291-
# The default value is derived from the ASCII encoding of "ttkclean" (Token Transaction Keystore Cleanup).
292-
# Only change this if you need to run multiple independent cleanup managers on the same database.
293-
# Note: PostgreSQL advisory locks use 64-bit integers. This value must be unique across your application.
294-
advisoryLockID: 8389190333894887277
295-
296288
# instanceID identifies this replica in logs and monitoring.
297289
# If empty, a unique identifier is generated automatically at startup.
298290
# Set this explicitly in containerized environments for consistent identity across restarts.
@@ -676,7 +668,6 @@ token:
676668
scanInterval: 1h
677669
batchSize: 100
678670
workerCount: 1
679-
advisoryLockID: 8389190333894887277
680671
instanceID:
681672
```
682673

@@ -687,15 +678,14 @@ Default values:
687678
- scanInterval: 1h
688679
- batchSize: 100
689680
- workerCount: 1
690-
- advisoryLockID: 8389190333894887277 (`0x74746b636c65616e`)
691681
- instanceID: empty, auto-generated when the cleanup manager starts
692682

693683
**Parameter Relationships and Tuning:**
694684

695685
- **Cleanup is disabled by default** and must be explicitly enabled. This is a conservative default to prevent unexpected key deletion in existing deployments.
696686
- **Only deleted tokens older than `ttl` are considered for cleanup** to ensure tokens are truly finalized before key deletion.
697687
- **The manager validates** that `ttl`, `scanInterval`, `batchSize`, and `workerCount` are all greater than zero.
698-
- **`advisoryLockID`** is used to acquire PostgreSQL advisory-lock leadership so that only one replica performs a cleanup sweep at a time. The default value (8389190333894887277 or 0x74746b636c65616e) represents the ASCII string "ttkclean" (Token Transaction Keystore Cleanup) encoded as a 64-bit integer.
688+
- **Cleanup leadership** uses a PostgreSQL advisory lock so only one replica sweeps at a time. The identifier is not configurable: it is derived from the TMS's own token SKI cleanup table name, which already carries network, channel and namespace, so it is unique per TMS automatically.
699689
- **`instanceID`** is used to identify this replica in logs and monitoring; if omitted, the manager generates a unique identifier automatically at startup.
700690

701691
**Tuning Recommendations:**
@@ -717,7 +707,6 @@ Default values:
717707

718708
4. **For Multi-Instance Deployments:**
719709
- **PostgreSQL Required**: Multi-instance deployments require PostgreSQL for distributed coordination via advisory locks
720-
- Keep default `advisoryLockID` unless running multiple independent cleanup systems
721710
- Consider setting explicit `instanceID` values for easier debugging and monitoring
722711

723712
5. **For Single-Node Deployments:**
@@ -794,7 +783,6 @@ Default values:
794783

795784
4. **For Multi-Instance Deployments:**
796785
- **PostgreSQL Required**: Multi-instance deployments require PostgreSQL for distributed coordination via advisory locks
797-
- Keep default `advisoryLockID` unless running multiple independent recovery systems
798786
- Consider setting explicit `instanceID` values for easier debugging and monitoring
799787
- Ensure all instances share the same PostgreSQL database for proper coordination
800788

token/services/storage/db/driver/token.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ type TokenStore interface {
287287
// Returns (leadership, true, nil) if leadership was acquired.
288288
// Returns (nil, false, nil) if leadership is held by another instance.
289289
// Returns (nil, false, error) if an error occurred.
290-
AcquireCleanupLeadership(ctx context.Context, lockID int64) (CleanupLeadership, bool, error)
290+
AcquireCleanupLeadership(ctx context.Context) (CleanupLeadership, bool, error)
291291
}
292292

293293
type (

token/services/storage/db/sql/common/tokens.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ type TokenStore struct {
4747
table tokenTables
4848
ci common3.CondInterpreter
4949
notifier driver.TokenNotifier
50-
cleanupLeaderFactory func(context.Context, *sql.DB, int64) (driver.CleanupLeadership, bool, error)
50+
cleanupLeaderFactory func(context.Context, *sql.DB) (driver.CleanupLeadership, bool, error)
5151

5252
sttMutex sync.RWMutex
5353
supportedTokenFormats []token.Format
@@ -68,7 +68,7 @@ type TokenStore struct {
6868
balanceStmts PreparedStmtHolder[string]
6969
}
7070

71-
func newTokenStore(readDB, writeDB *sql.DB, tables tokenTables, ci common3.CondInterpreter, notifier driver.TokenNotifier, cleanupLeaderFactory func(context.Context, *sql.DB, int64) (driver.CleanupLeadership, bool, error)) *TokenStore {
71+
func newTokenStore(readDB, writeDB *sql.DB, tables tokenTables, ci common3.CondInterpreter, notifier driver.TokenNotifier, cleanupLeaderFactory func(context.Context, *sql.DB) (driver.CleanupLeadership, bool, error)) *TokenStore {
7272
ts := &TokenStore{
7373
readDB: readDB,
7474
writeDB: writeDB,
@@ -100,7 +100,7 @@ func NewTokenStoreWithNotifierAndCleanup(
100100
tables TableNames,
101101
ci common3.CondInterpreter,
102102
notifier driver.TokenNotifier,
103-
cleanupLeaderFactory func(context.Context, *sql.DB, int64) (driver.CleanupLeadership, bool, error),
103+
cleanupLeaderFactory func(context.Context, *sql.DB) (driver.CleanupLeadership, bool, error),
104104
) (*TokenStore, error) {
105105
return newTokenStore(readDB, writeDB, tokenTables{
106106
Tokens: tables.Tokens,
@@ -1359,12 +1359,12 @@ func (db *TokenStore) MarkTokenCleaned(ctx context.Context, txID string, index u
13591359
// In distributed deployments (PostgreSQL), this uses advisory locks to ensure only one instance performs cleanup.
13601360
// AcquireCleanupLeadership returns a leadership handle for cleanup sweeping.
13611361
// When no leader factory is configured, leadership is granted locally.
1362-
func (db *TokenStore) AcquireCleanupLeadership(ctx context.Context, lockID int64) (driver.CleanupLeadership, bool, error) {
1362+
func (db *TokenStore) AcquireCleanupLeadership(ctx context.Context) (driver.CleanupLeadership, bool, error) {
13631363
if db.cleanupLeaderFactory == nil {
13641364
return noopCleanupLeadership{}, true, nil
13651365
}
13661366

1367-
return db.cleanupLeaderFactory(ctx, db.writeDB, lockID)
1367+
return db.cleanupLeaderFactory(ctx, db.writeDB)
13681368
}
13691369

13701370
// noopCleanupLeadership is a no-op implementation for non-distributed deployments

token/services/storage/db/sql/postgres/advisorylock.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/LFDT-Panurus/panurus/token/services/logging"
1414
tokensdriver "github.com/LFDT-Panurus/panurus/token/services/storage/db/driver"
15+
common5 "github.com/LFDT-Panurus/panurus/token/services/storage/db/sql/common"
1516
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
1617
"github.com/hyperledger-labs/fabric-smart-client/platform/common/utils"
1718
)
@@ -103,16 +104,12 @@ func (l *AdvisoryLock) Close() error {
103104
return nil
104105
}
105106

106-
// NewAdvisoryLockFactory returns a recovery leader factory function that uses PostgreSQL advisory locks.
107-
func NewAdvisoryLockFactory() func(context.Context, *sql.DB, int64) (tokensdriver.RecoveryLeadership, bool, error) {
108-
return func(ctx context.Context, db *sql.DB, lockID int64) (tokensdriver.RecoveryLeadership, bool, error) {
109-
lock, acquired, err := NewAdvisoryLock(ctx, db, lockID)
110-
if err != nil || !acquired {
111-
return nil, acquired, err
112-
}
113-
114-
return lock, true, nil
115-
}
107+
// recoveryLockID derives the advisory lock id guarding a TMS's recovery sweep from its fully
108+
// qualified requests table name, which already carries the network, channel and namespace. The
109+
// table prefix alone is not enough: TMSes normally share one persistence configuration and are
110+
// distinguished only by those params, so a prefix-derived id would collide across them.
111+
func recoveryLockID(tables common5.TableNames) int64 {
112+
return createTableLockID(tables.Requests + "_recovery")
116113
}
117114

118115
// NewAdvisoryLockFactoryForID returns a recovery leader factory bound to lockID. Binding the id
@@ -129,9 +126,17 @@ func NewAdvisoryLockFactoryForID(lockID int64) func(context.Context, *sql.DB) (t
129126
}
130127
}
131128

132-
// NewCleanupLeaderFactory returns a cleanup leader factory function that uses PostgreSQL advisory locks.
133-
func NewCleanupLeaderFactory() func(context.Context, *sql.DB, int64) (tokensdriver.CleanupLeadership, bool, error) {
134-
return func(ctx context.Context, db *sql.DB, lockID int64) (tokensdriver.CleanupLeadership, bool, error) {
129+
// keystoreCleanupLockID derives the advisory lock id guarding a TMS's keystore cleanup sweep from
130+
// its fully qualified token SKI cleanup table name, for the same reason as recoveryLockID: the
131+
// table prefix alone is shared by every TMS on one persistence configuration.
132+
func keystoreCleanupLockID(tables common5.TableNames) int64 {
133+
return createTableLockID(tables.TokenSKICleanups + "_cleanup")
134+
}
135+
136+
// NewCleanupLeaderFactoryForID returns a cleanup leader factory bound to lockID, so the id is
137+
// fixed when the store is built rather than supplied per call.
138+
func NewCleanupLeaderFactoryForID(lockID int64) func(context.Context, *sql.DB) (tokensdriver.CleanupLeadership, bool, error) {
139+
return func(ctx context.Context, db *sql.DB) (tokensdriver.CleanupLeadership, bool, error) {
135140
lock, acquired, err := NewAdvisoryLock(ctx, db, lockID)
136141
if err != nil || !acquired {
137142
return nil, acquired, err

token/services/storage/db/sql/postgres/advisorylock_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,20 +164,19 @@ func TestAdvisoryLockFactory(t *testing.T) {
164164
defer utils.IgnoreErrorFunc(db.Close)
165165

166166
ctx := context.Background()
167-
lockID := int64(77777)
168167

169-
// Test factory function
170-
factory := NewAdvisoryLockFactory()
168+
// The factory binds its lock id at construction, so callers cannot pass one in.
169+
factory := NewAdvisoryLockFactoryForID(int64(77777))
171170
require.NotNil(t, factory)
172171

173172
// Use factory to create lock
174-
lock, acquired, err := factory(ctx, db, lockID)
173+
lock, acquired, err := factory(ctx, db)
175174
require.NoError(t, err)
176175
require.True(t, acquired)
177176
require.NotNil(t, lock)
178177

179178
// Verify it's actually locked
180-
lock2, acquired, err := factory(ctx, db, lockID)
179+
lock2, acquired, err := factory(ctx, db)
181180
require.NoError(t, err)
182181
require.False(t, acquired)
183182
require.Nil(t, lock2)

token/services/storage/db/sql/postgres/recovery_lockid_test.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ func tmsTables(t *testing.T, prefix, network, channel, namespace string) sqlcomm
2323
return tables
2424
}
2525

26-
// recoveryLockID mirrors the derivation in NewTransactionStoreWithNotifier.
27-
func recoveryLockID(tables sqlcommon.TableNames) int64 {
28-
return createTableLockID(tables.Requests + "_recovery")
29-
}
30-
3126
// Two TMSes normally share one persistence configuration and differ only by the network,
3227
// channel and namespace params, so the recovery lock id has to be derived from something that
3328
// carries them. Deriving from the table prefix alone would give both the same id, and every

token/services/storage/db/sql/postgres/tokens.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (n *TokenNotifier) Subscribe(callback func(tokensdriver.Operation, tokensdr
7373

7474
func NewTokenStoreWithNotifier(dbs *scommon.RWDB, tableNames sqlcommon.TableNames, notifier *TokenNotifier) (*TokenStore, error) {
7575
// Create cleanup leader factory using PostgreSQL advisory locks
76-
cleanupLeaderFactory := NewCleanupLeaderFactory()
76+
cleanupLeaderFactory := NewCleanupLeaderFactoryForID(keystoreCleanupLockID(tableNames))
7777

7878
baseStore, err := sqlcommon.NewTokenStoreWithNotifierAndCleanup(
7979
dbs.ReadDB,

token/services/storage/db/sql/postgres/transactions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func NewTransactionStoreWithNotifier(dbs *scommon.RWDB, tableNames sqlcommon.Tab
7474
// the table prefix would not be enough: several TMSes normally share one persistence
7575
// configuration and are distinguished only by those params, so they would all land on the same
7676
// id and every TMS but one would skip its sweep. See #1798 for the same bug on the token locks.
77-
recoveryLeaderFactory := NewAdvisoryLockFactoryForID(createTableLockID(tableNames.Requests + "_recovery"))
77+
recoveryLeaderFactory := NewAdvisoryLockFactoryForID(recoveryLockID(tableNames))
7878

7979
commonStore, err := sqlcommon.NewTransactionStoreWithNotifierAndRecovery(
8080
dbs.ReadDB,

token/services/storage/services/cleanup/config.go

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,18 @@ type Config struct {
2929
BatchSize int
3030
// WorkerCount is the number of local workers processing tokens
3131
WorkerCount int
32-
// AdvisoryLockID is the PostgreSQL advisory lock ID used for leader election
33-
AdvisoryLockID int64
3432
// InstanceID identifies the current replica as the cleanup owner
3533
InstanceID string
3634
}
3735

38-
const (
39-
defaultLockID int64 = 0x74746b636c65616e // "ttkclean" in hex
40-
)
41-
4236
// DefaultConfig returns the default cleanup configuration
4337
func DefaultConfig() Config {
4438
return Config{
45-
Enabled: false, // Disabled by default - must be explicitly enabled
46-
TTL: 24 * time.Hour, // Wait 24 hours before cleaning up keys
47-
ScanInterval: 1 * time.Hour, // Scan every hour
48-
BatchSize: 100,
49-
WorkerCount: 1,
50-
AdvisoryLockID: defaultLockID,
39+
Enabled: false, // Disabled by default - must be explicitly enabled
40+
TTL: 24 * time.Hour, // Wait 24 hours before cleaning up keys
41+
ScanInterval: 1 * time.Hour, // Scan every hour
42+
BatchSize: 100,
43+
WorkerCount: 1,
5144
}
5245
}
5346

@@ -81,9 +74,6 @@ func LoadConfig(cfg *config.Configuration) (Config, error) {
8174
if config.WorkerCount > 0 {
8275
result.WorkerCount = config.WorkerCount
8376
}
84-
if config.AdvisoryLockID != 0 {
85-
result.AdvisoryLockID = config.AdvisoryLockID
86-
}
8777
if config.InstanceID != "" {
8878
result.InstanceID = config.InstanceID
8979
}

token/services/storage/services/cleanup/manager.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type DeletedToken struct {
3131
// Storage defines the interface for querying deleted tokens
3232
type Storage interface {
3333
// AcquireCleanupLeadership acquires an advisory lock for cleanup leadership
34-
AcquireCleanupLeadership(ctx context.Context, lockID int64) (Leadership, bool, error)
34+
AcquireCleanupLeadership(ctx context.Context) (Leadership, bool, error)
3535
// GetDeletedTokensPendingSKICleanup returns deleted tokens older than the specified duration that haven't had their SKI keys cleaned.
3636
// Only tokens owned by this node are returned, since this node only holds the secret keys for tokens it owns.
3737
GetDeletedTokensPendingSKICleanup(ctx context.Context, olderThan time.Duration, limit int) ([]DeletedToken, error)
@@ -139,8 +139,8 @@ func (m *Manager) Start() error {
139139
m.wg.Add(1)
140140
go m.cleanupLoop()
141141

142-
m.logger.Infof("keystore cleanup manager started (TTL: %s, Scan Interval: %s, Batch Size: %d, Workers: %d, Lock ID: %d, Instance ID: %s)",
143-
m.config.TTL, m.config.ScanInterval, m.config.BatchSize, m.config.WorkerCount, m.config.AdvisoryLockID, m.config.InstanceID)
142+
m.logger.Infof("keystore cleanup manager started (TTL: %s, Scan Interval: %s, Batch Size: %d, Workers: %d, Instance ID: %s)",
143+
m.config.TTL, m.config.ScanInterval, m.config.BatchSize, m.config.WorkerCount, m.config.InstanceID)
144144

145145
return nil
146146
}
@@ -205,7 +205,7 @@ func (m *Manager) validateConfig() error {
205205
}
206206

207207
func (m *Manager) runSweep(ctx context.Context) error {
208-
leadership, acquired, err := m.storage.AcquireCleanupLeadership(ctx, m.config.AdvisoryLockID)
208+
leadership, acquired, err := m.storage.AcquireCleanupLeadership(ctx)
209209
if err != nil {
210210
return errors.Wrapf(err, "failed to acquire cleanup leadership")
211211
}

0 commit comments

Comments
 (0)