testutils

package
v0.27.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Index

Constants

View Source
const (
	NumTokensPerWallet         = 1000000
	TokenQuantityPrecision     = 64
	SelectQuantity             = "100"
	TxID                       = "someTxID"
	TokenType                  = "USD"
	SelectorNumRetries         = 3
	SelectorTimeout            = 5 * time.Second
	LockSleepTimeout           = 2 * time.Second
	LockValidTxEvictionTimeout = 5 * time.Minute
)

Variables

View Source
var (

	// SimpleDriverTokenFilter is defaultTokenFilter's counterpart for the simple
	// driver: it must carry a non-empty WalletID matching the identity every token
	// is stored under (see UpdateTokens' hardcoded []string{"alice"} identity list
	// below), since simple's selector.Select rejects an empty ownerFilter.ID() and
	// uses it directly as the SQL walletID filter (unlike sherdlock's ContainsToken
	// fallback). Used by TestHotTokenContentionSimpleDriver
	// (token/services/selector/simple/contention_test.go) via
	// TestHotTokenContentionNWithFilter.
	SimpleDriverTokenFilter = &TokenFilter{Wallet: defaultWalletOwner, WalletID: "alice"}
)

Functions

func NewEnhancedManager

func NewEnhancedManager(t *testing.T, manager token2.SelectorManager, tokenDB driver.TokenStore) *enhancedManager

func TestHotTokenContention added in v0.24.0

func TestHotTokenContention(t *testing.T, replicas []EnhancedManager)

TestHotTokenContention mirrors the incident reported in #2395: a wallet with a few small tokens and one much larger one, and far more concurrent requests than tokens. Every request asks for CHF1, satisfiable either by a small token directly or by the large one — which recycles most of its value back as a freshly-minted token via deleteTokensAndStoreChange, so at any instant there is exactly one "big" token in the pool. That is the hot token every losing goroutine keeps re-targeting after a lost lock race, absent a per-attempt blacklist (mechanism 1 in #2395) or an anti-join against already-locked tokens (mechanism 3).

Unlike the other cases in this file, callers are expected to also inspect lock-conflict counts (see sherdlock's TestHotTokenContention, which wraps the Locker to record them) and assert on their distribution — this function only asserts the functional invariant that must hold regardless of how contention is distributed: total demand exactly matches the wallet balance, so no error here can be a genuine insufficient-funds; any error is spurious, caused by contention.

func TestHotTokenContentionN added in v0.26.0

func TestHotTokenContentionN(t *testing.T, replicas []EnhancedManager, requestsPerReplica int)

TestHotTokenContentionN is TestHotTokenContention parameterized by requestsPerReplica (TestHotTokenContention itself is just requestsPerReplica=100), so a caller whose driver has different concurrency characteristics can scale the workload down to something that completes in a reasonable time while keeping the same token-mix shape (a handful of small tokens plus one much larger, rotating "hot" one, demand exactly equal to wallet balance). See simple/contention_test.go's TestHotTokenContentionSimpleDriver: the simple driver's selector re-scans its whole candidate set from scratch on every lost lock race (no per-attempt blacklist, unlike sherdlock), so the original 3x100 shape takes far longer to settle against it than against sherdlock/Postgres.

func TestHotTokenContentionNWithFilter added in v0.26.0

func TestHotTokenContentionNWithFilter(t *testing.T, replicas []EnhancedManager, requestsPerReplica int, filter token2.OwnerFilter)

TestHotTokenContentionNWithFilter is TestHotTokenContentionN parameterized by the OwnerFilter passed to Select, so callers whose driver requires a non-empty ownerFilter.ID() (e.g. the simple driver — see SimpleDriverTokenFilter) can supply one without affecting sherdlock's tests, which rely on defaultTokenFilter's empty WalletID.

func TestHotTokenContentionWideWindow added in v0.26.0

func TestHotTokenContentionWideWindow(t *testing.T, replicas []EnhancedManager)

TestHotTokenContentionWideWindow targets the case TestHotTokenContention structurally cannot: a wallet made entirely of CHF1 dust, with every request costing CHF3, so selectInternal's covering-window loop (selector.go) always needs three ascending candidates to satisfy one request, never one. TestHotTokenContention's rotating big token means almost every claim - after the initial handful of small tokens are spent - is a single token that alone covers the request, so its window is size 1 for nearly the entire run: exactly the case where FOR UPDATE SKIP LOCKED, which only skips *other* candidates present in the same statement, cannot show any benefit over a plain INSERT. Here there is no dominant token to fall back to, so every one of the run's many concurrent claims genuinely contends over which three dust tokens, among many similarly-ranked ones, it gets to walk away with - the scenario Phase 6's skipLocked strategy is meant to help.

func TestHotTokenContentionWithSettlement added in v0.26.0

func TestHotTokenContentionWithSettlement(t *testing.T, replicas []EnhancedManager, lockDB driver.TokenLockStore)

TestHotTokenContentionWithSettlement is TestHotTokenContention with a settlement step spliced in after each successful Select+spend: it releases the winning transaction's locks through a *real* finality.SelectorManagerProvider, wired via depmock.TokenManagementServiceProvider/TokenManagementServiceWithExtensions to resolve to replica itself (every EnhancedManager already satisfies token2.SelectorManager) - the same provider chain finality.Listener.releaseLocks uses on tx confirmation (finality/listener.go:186-208). TestHotTokenContention's Close-only harness never touches the lock table (Close just evicts the selector from cache, sherdlock/manager.go:78-84), so it can only simulate mechanism 4's leak, never prove its fix. lockDB is the TokenLockStore backing every replica's Locker - callers construct their replicas over one shared table, so any one of them will do - used only for the final assertion: after every request has settled, ListLocks must report nothing at all, regardless of which replica won or how many lock conflicts it took.

func TestInsufficientTokensManyReplicas

func TestInsufficientTokensManyReplicas(t *testing.T, replicas []EnhancedManager)

func TestInsufficientTokensOneReplica

func TestInsufficientTokensOneReplica(t *testing.T, replica EnhancedManager)

func TestStaticHotTokenContentionPareto added in v0.26.0

func TestStaticHotTokenContentionPareto(t *testing.T, replicas []EnhancedManager, lockDB driver.TokenLockStore, requestsPerReplica int) []token.ID

TestStaticHotTokenContentionPareto targets the shape TestHotTokenContentionWithSettlement's rotating hot token structurally cannot: stress's actual incident had a small, static set of tokens - never spent, only locked and released - absorbing the overwhelming majority of lock conflicts (one of them contested well over a thousand times in a few minutes). A rotating hot token (see TestHotTokenContention's doc comment) dilutes any single ID's share by construction, since deleteTokensAndStoreChange mints a fresh ID every time it is spent. Here, every winning request releases its lock via the real SelectorManager.Unlock path (like parallelSelectWithSettlement) but never spends the token, so the same handful of token IDs stay in the pool, and stay the top candidates, for the entire run.

The wallet mix is the fixed hot set plus a "cold" pool stored under a different token type: Select's query is scoped by (walletID, tokenType), and defaultTokenFilter deliberately leaves WalletID empty (see its doc comment - sherdlock's SQL path treats that as "no owner filter"), so tokenType is the only scope this harness can actually rely on to keep the cold pool out of the candidate set entirely. An earlier version of this scenario instead relied on a much larger cold denomination to fall outside nextCandidate's maxSufficiencyRatio lookahead window (selector.go) - but that only stops cold from being pulled into a window anchored on a hot candidate; once every hot token is momentarily locked by other requests (unsurprising at this concurrency, against only numHotTokens candidates) the cold pool becomes the next visible individually-sufficient candidate in its own right and gets attempted anyway, diluting the hot set's measured share well below any believable concentration floor. Scoping by tokenType instead is enforced by the query itself, so the concentration this test asserts on is deterministic and not a share of one: the wallet genuinely holds other tokens, they are simply outside the scope of the requests under test, exactly as another currency's tokens in the same production DB would be.

Callers (see sherdlock's TestStaticHotTokenContentionPareto, which wraps the Locker to record per-token attempt/conflict counts) are expected to assert on the concentration of conflicts among the returned hot token IDs. This function only asserts the invariants that must hold regardless of how contention is distributed: no error here can be a genuine insufficient- funds (every request's amount is well within a single hot token, and nothing is ever spent), and after the run every lock is released. Unlike TestHotTokenContention, it cannot also assert "no spurious insufficient-funds under spend" as a proxy for correctness, since nothing is ever spent here - it is testing contention shape, not the spend path.

func TestSufficientTokensBigDenominationsManyReplicas

func TestSufficientTokensBigDenominationsManyReplicas(t *testing.T, replicas []EnhancedManager)

func TestSufficientTokensBigDenominationsOneReplica

func TestSufficientTokensBigDenominationsOneReplica(t *testing.T, replica EnhancedManager)

func TestSufficientTokensManyReplicas

func TestSufficientTokensManyReplicas(t *testing.T, replicas []EnhancedManager)

func TestSufficientTokensOneReplica

func TestSufficientTokensOneReplica(t *testing.T, replica EnhancedManager)

Types

type EnhancedManager

type EnhancedManager interface {
	token2.SelectorManager
	TokenSum() (token.Quantity, error)
	UpdateTokens(spentTokens []*token.ID, addedTokens []token.UnspentToken) error
}

type MockIterator

type MockIterator struct {
	// contains filtered or unexported fields
}

func (*MockIterator) Close

func (m *MockIterator) Close()

func (*MockIterator) Next

func (m *MockIterator) Next() (*token2.UnspentToken, error)

type MockQueryService

type MockQueryService struct {
	// contains filtered or unexported fields
}

func NewMockQueryService

func NewMockQueryService() *MockQueryService

func (*MockQueryService) Add

func (q *MockQueryService) Add(key string, t *token2.UnspentToken)

func (*MockQueryService) GetStatus

func (q *MockQueryService) GetStatus(ctx context.Context, txID string) (token.TxStatus, string, error)

func (*MockQueryService) GetTokens

func (q *MockQueryService) GetTokens(ctx context.Context, inputs ...*token2.ID) ([]*token2.Token, error)

func (*MockQueryService) GetUnspentToken

func (q *MockQueryService) GetUnspentToken(_ context.Context, tokenID *token2.ID) *token2.UnspentToken

func (*MockQueryService) GetUnspentTokens

func (q *MockQueryService) GetUnspentTokens(_ context.Context, inputs ...*token2.ID) ([]*token2.UnspentToken, error)

func (*MockQueryService) HasEnoughSpendableTokens added in v0.26.0

func (q *MockQueryService) HasEnoughSpendableTokens(_ context.Context, walletID string, typ token2.Type, target *big.Int) (bool, error)

HasEnoughSpendableTokens reports whether the sum of walletID's cached tokens of typ is at least target, mirroring SpendableTokensIteratorBy's ignore-locks semantics (this mock has no lock concept at all).

func (*MockQueryService) SpendableTokensIteratorBy

func (q *MockQueryService) SpendableTokensIteratorBy(ctx context.Context, walletID string, typ token2.Type) (driver.SpendableTokensIterator, error)

func (*MockQueryService) UnspentTokensIterator

func (q *MockQueryService) UnspentTokensIterator(context.Context) (*token.UnspentTokensIterator, error)

func (*MockQueryService) UnspentTokensIteratorBy

func (q *MockQueryService) UnspentTokensIteratorBy(_ context.Context, walletID string, _ token2.Type) (driver.UnspentTokensIterator, error)

func (*MockQueryService) WarmupCache

func (q *MockQueryService) WarmupCache(walletID, tokenType string)

type MockVault

type MockVault struct {
}

func (*MockVault) GetStatus

func (m *MockVault) GetStatus(ctx context.Context, txID string) (ttxdb.TxStatus, string, error)

type NoLock

type NoLock struct {
}

func (*NoLock) IsLocked

func (n *NoLock) IsLocked(id *token2.ID) bool

func (*NoLock) Lock

func (n *NoLock) Lock(_ context.Context, _ string, _ *token2.ID, _ string, _ bool) (string, error)

func (*NoLock) UnlockByTxID

func (n *NoLock) UnlockByTxID(ctx context.Context, txID string)

func (*NoLock) UnlockIDs

func (n *NoLock) UnlockIDs(_ context.Context, _ string, ids ...*token2.ID) []*token2.ID

type TokenFilter

type TokenFilter struct {
	Wallet   []byte
	WalletID string
}

func (*TokenFilter) ContainsToken

func (c *TokenFilter) ContainsToken(token *token2.UnspentToken) bool

func (*TokenFilter) ID

func (c *TokenFilter) ID() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL