Documentation
¶
Index ¶
- Constants
- Variables
- func NewEnhancedManager(t *testing.T, manager token2.SelectorManager, tokenDB driver.TokenStore) *enhancedManager
- func TestHotTokenContention(t *testing.T, replicas []EnhancedManager)
- func TestHotTokenContentionN(t *testing.T, replicas []EnhancedManager, requestsPerReplica int)
- func TestHotTokenContentionNWithFilter(t *testing.T, replicas []EnhancedManager, requestsPerReplica int, ...)
- func TestHotTokenContentionWideWindow(t *testing.T, replicas []EnhancedManager)
- func TestHotTokenContentionWithSettlement(t *testing.T, replicas []EnhancedManager, lockDB driver.TokenLockStore)
- func TestInsufficientTokensManyReplicas(t *testing.T, replicas []EnhancedManager)
- func TestInsufficientTokensOneReplica(t *testing.T, replica EnhancedManager)
- func TestStaticHotTokenContentionPareto(t *testing.T, replicas []EnhancedManager, lockDB driver.TokenLockStore, ...) []token.ID
- func TestSufficientTokensBigDenominationsManyReplicas(t *testing.T, replicas []EnhancedManager)
- func TestSufficientTokensBigDenominationsOneReplica(t *testing.T, replica EnhancedManager)
- func TestSufficientTokensManyReplicas(t *testing.T, replicas []EnhancedManager)
- func TestSufficientTokensOneReplica(t *testing.T, replica EnhancedManager)
- type EnhancedManager
- type MockIterator
- type MockQueryService
- func (q *MockQueryService) Add(key string, t *token2.UnspentToken)
- func (q *MockQueryService) GetStatus(ctx context.Context, txID string) (token.TxStatus, string, error)
- func (q *MockQueryService) GetTokens(ctx context.Context, inputs ...*token2.ID) ([]*token2.Token, error)
- func (q *MockQueryService) GetUnspentToken(_ context.Context, tokenID *token2.ID) *token2.UnspentToken
- func (q *MockQueryService) GetUnspentTokens(_ context.Context, inputs ...*token2.ID) ([]*token2.UnspentToken, error)
- func (q *MockQueryService) HasEnoughSpendableTokens(_ context.Context, walletID string, typ token2.Type, target *big.Int) (bool, error)
- func (q *MockQueryService) SpendableTokensIteratorBy(ctx context.Context, walletID string, typ token2.Type) (driver.SpendableTokensIterator, error)
- func (q *MockQueryService) UnspentTokensIterator(context.Context) (*token.UnspentTokensIterator, error)
- func (q *MockQueryService) UnspentTokensIteratorBy(_ context.Context, walletID string, _ token2.Type) (driver.UnspentTokensIterator, error)
- func (q *MockQueryService) WarmupCache(walletID, tokenType string)
- type MockVault
- type NoLock
- type TokenFilter
Constants ¶
Variables ¶
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) 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 TokenFilter ¶
func (*TokenFilter) ContainsToken ¶
func (c *TokenFilter) ContainsToken(token *token2.UnspentToken) bool
func (*TokenFilter) ID ¶
func (c *TokenFilter) ID() string