Documentation
¶
Overview ¶
Package locktest provides a controllable in-memory Driver implementation and a conformance test suite for use in unit tests.
Conformance suites ¶
Two suites are provided to separate concerns:
RunDriverConformance: semantic correctness (C-1..C-4, C-7). Runs against any Driver, including FakeDriver. No real-time waits; safe for -race.
RunDriverTTLConformance: TTL physics (C-5, C-6). Uses real time.Sleep because it is testing real-clock TTL expiry on backends. Intended for integration tests (e.g., adapters/redis/integration_test.go) where wall-clock waits are acceptable.
FakeDriver conformance tests (conformance_test.go) call only RunDriverConformance. Redis integration tests call both.
Package locktest provides a controllable in-memory Driver implementation and a conformance test suite for use in unit tests.
FakeDriver implements runtime/distlock.Driver in memory and is intended for unit tests of the distlock manager. RunDriverConformance verifies that any Driver implementation behaves identically for token ownership semantics.
Clock injection warning ¶
NewFakeDriver uses real time.Now for TTL expiry checks by default. When testing alongside FakeClock for the manager, you MUST call NewFakeDriverWithClock(fc.Now) (or fd.WithClock(fc.Now)) on the FakeDriver as well — otherwise the manager's logical clock and the driver's TTL clock will diverge, causing intermittent test failures.
Index ¶
- Variables
- func RunDriverConformance(t *testing.T, factory DriverFactory)
- func RunDriverTTLConformance(t *testing.T, factory DriverFactory)
- type DriverFactory
- type FakeDriver
- func (fd *FakeDriver) BlockNextRenew() <-chan struct{}
- func (fd *FakeDriver) Calls(method string) int
- func (fd *FakeDriver) ClearRenewError()
- func (fd *FakeDriver) LastRenewDeadline() time.Time
- func (fd *FakeDriver) Release(_ context.Context, key, token string) error
- func (fd *FakeDriver) Renew(ctx context.Context, key, token string, ttl time.Duration) (bool, error)
- func (fd *FakeDriver) ResetCalls()
- func (fd *FakeDriver) SetNX(_ context.Context, key, token string, ttl time.Duration) (bool, error)
- func (fd *FakeDriver) SetNextReleaseError(err error)
- func (fd *FakeDriver) SetNextRenewError(err error)
- func (fd *FakeDriver) SetNextRenewHeld(held bool)
- func (fd *FakeDriver) SetNextSetNX(acquired bool)
- func (fd *FakeDriver) SetRenewErrorPersistent(err error)
- func (fd *FakeDriver) Snapshot() map[string]string
- func (fd *FakeDriver) UnblockRenew()
- func (fd *FakeDriver) WithClock(now func() time.Time) *FakeDriver
Constants ¶
This section is empty.
Variables ¶
var ErrDriverIO = errors.New("locktest: simulated driver I/O error")
ErrDriverIO is a sentinel error for injecting I/O failures in tests.
Functions ¶
func RunDriverConformance ¶
func RunDriverConformance(t *testing.T, factory DriverFactory)
RunDriverConformance runs the semantic correctness conformance suite (C-1..C-4, C-7) against the supplied factory. Both FakeDriver (locktest) and RedisDriver (adapters/redis) must pass this suite.
This suite makes no real-time waits and is safe to run with -race.
Mirrors the outboxtest.RunStoreConformanceSuite pattern.
func RunDriverTTLConformance ¶
func RunDriverTTLConformance(t *testing.T, factory DriverFactory)
RunDriverTTLConformance runs the TTL physics conformance suite (C-5, C-6) against the supplied factory. These tests use real time.Sleep because they are validating actual backend TTL expiry.
Call this from integration tests where wall-clock waits are acceptable (e.g., adapters/redis/integration_test.go). Do NOT call it for FakeDriver conformance — FakeDriver's TTL expiry is clock-injected and tested via the manager's FakeClock-driven renew cycles instead.
C-5 also covers the Lock.Orphan() backend expectation: after renewal stops (no Renew calls, no Release call), the key persists and expires only via TTL, at which point SetNX succeeds for a competitor. The Driver has no Orphan method — the orphan semantic lives entirely in the manager layer (handleOrphan stops renewal without any Driver I/O); C-5 validates the underlying physics that makes that contract possible.
Types ¶
type DriverFactory ¶
DriverFactory returns a fresh Driver and its initial clock for each test sub-case. For real backends the factory should set up a clean namespace/key prefix.
type FakeDriver ¶
type FakeDriver struct {
// contains filtered or unexported fields
}
FakeDriver is a thread-safe in-memory implementation of distlock.Driver. It is intended for unit tests only.
Controls:
- NextSetNXResult: if set to false, the next SetNX returns (false, nil) — simulates busy key
- NextRenewError: if non-nil, the next Renew call returns (false, err) — single-shot
- persistRenewError: if non-nil, every Renew call returns (false, err) until ClearRenewError
- NextRenewHeld: if set to false, the next Renew returns (false, nil) — simulates ownership lost
- NextReleaseError: if non-nil, the next Release call returns err — single-shot
- blockRenewCh: if non-nil, the next Renew blocks until the channel is closed — use BlockNextRenew() + UnblockRenew() for controlled blocking/unblocking
Use NewFakeDriverWithClock when pairing with FakeClock to ensure the driver's TTL expiry logic uses the same logical time as the manager.
func NewFakeDriver ¶
func NewFakeDriver() *FakeDriver
NewFakeDriver creates a new FakeDriver using real-time clock.
Default uses real time.Now for TTL expiry checks. When testing alongside FakeClock for the manager, you MUST call WithClock(fc.Now) on the FakeDriver as well — otherwise the manager's logical clock and the driver's TTL clock will diverge, causing intermittent test failures.
Use NewFakeDriverWithClock for a one-step constructor that wires the clock.
func NewFakeDriverWithClock ¶
func NewFakeDriverWithClock(now func() time.Time) *FakeDriver
NewFakeDriverWithClock creates a new FakeDriver using the provided clock function for TTL expiry checks. Use this when pairing with FakeClock:
fc := locktest.NewFakeClock(time.Time{})
fd := locktest.NewFakeDriverWithClock(fc.Now)
func (*FakeDriver) BlockNextRenew ¶
func (fd *FakeDriver) BlockNextRenew() <-chan struct{}
BlockNextRenew arms a one-shot block: the next Renew call will block until UnblockRenew() is called (or the Renew context is canceled). The returned entered channel is closed by the Renew call once it has entered its body but before it blocks — tests can wait on it to confirm the goroutine is in-flight.
Usage:
entered := fd.BlockNextRenew() <-entered // Renew is now blocked inside FakeDriver lock.Orphan() // should not block even though Renew is in-flight fd.UnblockRenew() // let Renew complete
func (*FakeDriver) Calls ¶
func (fd *FakeDriver) Calls(method string) int
Calls returns the total number of times the named method was called. method is one of "SetNX", "Renew", "Release".
func (*FakeDriver) ClearRenewError ¶
func (fd *FakeDriver) ClearRenewError()
ClearRenewError clears both the single-shot and persistent renew error injections. After this call, Renew behaves normally (uses in-memory state).
func (*FakeDriver) LastRenewDeadline ¶
func (fd *FakeDriver) LastRenewDeadline() time.Time
LastRenewDeadline returns the deadline extracted from the context passed to the most recent Renew call. Returns the zero time if no Renew has been called or if the context carried no deadline.
Used by TestLocker_TC12_DriftFactor to assert that the Renew RPC context deadline reflects the configured drift factor:
deadline ≈ clock.Now() + ttl − drift
func (*FakeDriver) Release ¶
func (fd *FakeDriver) Release(_ context.Context, key, token string) error
Release implements distlock.Driver.
func (*FakeDriver) Renew ¶
func (fd *FakeDriver) Renew(ctx context.Context, key, token string, ttl time.Duration) (bool, error)
Renew implements distlock.Driver. Records the deadline from ctx for test introspection via LastRenewDeadline.
func (*FakeDriver) ResetCalls ¶
func (fd *FakeDriver) ResetCalls()
ResetCalls resets the call counters for all methods.
func (*FakeDriver) SetNextReleaseError ¶
func (fd *FakeDriver) SetNextReleaseError(err error)
SetNextReleaseError injects an I/O error for the next Release call (single-shot).
func (*FakeDriver) SetNextRenewError ¶
func (fd *FakeDriver) SetNextRenewError(err error)
SetNextRenewError injects an I/O error for the next Renew call (single-shot). After one Renew call consumes the error, subsequent calls behave normally (unless SetRenewErrorPersistent is also set).
func (*FakeDriver) SetNextRenewHeld ¶
func (fd *FakeDriver) SetNextRenewHeld(held bool)
SetNextRenewHeld injects the held result for the next Renew call. false simulates ownership lost (another holder took the key).
func (*FakeDriver) SetNextSetNX ¶
func (fd *FakeDriver) SetNextSetNX(acquired bool)
SetNextSetNX injects the result for the next SetNX call. false simulates "another holder owns the key".
func (*FakeDriver) SetRenewErrorPersistent ¶
func (fd *FakeDriver) SetRenewErrorPersistent(err error)
SetRenewErrorPersistent injects an I/O error that is returned by every Renew call until ClearRenewError is called. Use this to simulate persistent backend unavailability (e.g. to exhaust the retry budget in TC-14).
func (*FakeDriver) Snapshot ¶
func (fd *FakeDriver) Snapshot() map[string]string
Snapshot returns the current keys held by the FakeDriver. Used for white-box assertions in conformance tests.
func (*FakeDriver) UnblockRenew ¶
func (fd *FakeDriver) UnblockRenew()
UnblockRenew unblocks a previously blocked Renew call. Safe to call even if no Renew is currently blocked.
func (*FakeDriver) WithClock ¶
func (fd *FakeDriver) WithClock(now func() time.Time) *FakeDriver
WithClock replaces the time source used for TTL expiry. Useful when tests need to advance time to simulate TTL expiry.