lock

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Feb 28, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package lock provides cross-protocol translation helpers for lock visibility.

This file contains helpers for translating lock information between protocols (NLM and SMB), enabling cross-protocol conflict reporting and logging.

Use Cases:

  • When NLM TEST/LOCK fails due to SMB lease, translate lease to NLM holder info
  • When SMB lock/lease fails due to NLM lock, generate human-readable reason
  • Cross-protocol conflict logging at INFO level

Package lock provides lock management types and operations for the metadata package. This file contains SMB2/3 lease types integrated with the unified lock manager.

SMB2.1+ leases provide client caching permissions using Read/Write/Handle flags. Leases are whole-file (not byte-range) and use a 128-bit client-generated key to group multiple file handles into a single caching unit.

Reference: MS-SMB2 2.2.13.2.8 SMB2_CREATE_REQUEST_LEASE_V2

Package lock provides lock management types and operations for the metadata package. This file implements the lease break timeout scanner.

The OpLockBreakScanner monitors breaking leases and force-revokes them on timeout. Per MS-SMB2 and CONTEXT.md: "Force revoke on timeout - don't retry, just revoke and allow conflicting operation"

Package lock provides lock management types and operations for the metadata package. This package handles byte-range locking, deadlock detection, and lock persistence.

Import graph: errors <- lock <- metadata <- store implementations

Index

Constants

View Source
const (
	// LeaseStateNone indicates no caching is permitted.
	LeaseStateNone uint32 = 0x00

	// LeaseStateRead (SMB2_LEASE_READ_CACHING) permits caching reads.
	// Multiple clients can hold Read leases simultaneously.
	LeaseStateRead uint32 = 0x01

	// LeaseStateWrite (SMB2_LEASE_WRITE_CACHING) permits caching writes.
	// Only one client can hold a Write lease; requires exclusive access.
	// Client with Write lease has dirty data that must be flushed on break.
	LeaseStateWrite uint32 = 0x02

	// LeaseStateHandle (SMB2_LEASE_HANDLE_CACHING) permits caching open handles.
	// Client can delay close operations until another client needs access.
	LeaseStateHandle uint32 = 0x04
)

Lease state constants per MS-SMB2 2.2.13.2.8.

View Source
const (
	// DefaultOpLockBreakTimeout is the Windows default (35 seconds).
	// Per MS-SMB2 3.3.6.5: "implementation-specific default value in milliseconds"
	DefaultOpLockBreakTimeout = 35 * time.Second

	// OpLockBreakScanInterval is how often to check for expired breaks.
	OpLockBreakScanInterval = 1 * time.Second
)

Variables

ValidDirectoryLeaseStates contains valid lease state combinations for directories. Directories can only have Read or Read+Handle leases. Write leases are not permitted on directories.

ValidFileLeaseStates contains all valid lease state combinations for files. Per MS-SMB2: Write and Handle alone are not valid; they require Read. Valid combinations: None, R, RW, RH, RWH

Functions

func CheckIOConflict

func CheckIOConflict(existing *FileLock, sessionID uint64, offset, length uint64, isWrite bool) bool

CheckIOConflict checks if an I/O operation conflicts with an existing lock.

This implements SMB2 byte-range lock semantics per [MS-SMB2] 3.3.5.15:

  • Shared lock: Allows reads from all sessions but blocks writes from ALL sessions, including the lock holder. This is the key difference from POSIX advisory locks where a process's own locks never block its own I/O.
  • Exclusive lock: Only the lock holder can read or write the range.

Conflict rules:

  • READ + same session + any lock type = ALLOW
  • READ + different session + shared lock = ALLOW
  • READ + different session + exclusive lock = BLOCK
  • WRITE + same session + exclusive lock = ALLOW (lock holder can write)
  • WRITE + same session + shared lock = BLOCK (shared = read-only for everyone)
  • WRITE + different session + any lock = BLOCK

Parameters:

  • existing: The lock to check against
  • sessionID: The session performing the I/O
  • offset: Starting byte offset of the I/O
  • length: Number of bytes in the I/O
  • isWrite: true for write operations, false for reads

Returns true if the I/O is blocked by the existing lock.

func IsLockConflicting

func IsLockConflicting(existing, requested *FileLock) bool

IsLockConflicting checks if two locks conflict with each other.

Conflict rules:

  • Shared locks don't conflict with other shared locks (multiple readers)
  • Exclusive locks conflict with all other locks
  • Locks from the same session don't conflict (allows re-locking same range)
  • Ranges must overlap for a conflict to occur

Same-session re-locking: When a session requests a lock on a range it already holds, there is no conflict. This enables changing lock type on a range (e.g., shared -> exclusive) by acquiring a new lock that replaces the old one.

func IsUnifiedLockConflicting

func IsUnifiedLockConflicting(existing, requested *UnifiedLock) bool

IsUnifiedLockConflicting checks if two unified locks conflict with each other. It delegates to ConflictsWith which handles all conflict cases.

This standalone function is kept for backward compatibility and convenience in code that operates on two locks without a clear "this vs other" relationship.

func IsValidDirectoryLeaseState

func IsValidDirectoryLeaseState(state uint32) bool

IsValidDirectoryLeaseState returns true if the state is a valid lease combination for directories.

Valid directory states: None, R, RH Invalid: W, RW, H, WH, RWH (no Write caching for directories)

func IsValidFileLeaseState

func IsValidFileLeaseState(state uint32) bool

IsValidFileLeaseState returns true if the state is a valid lease combination for files.

Valid file states: None, R, RW, RH, RWH Invalid states: W alone, H alone, WH (Write/Handle without Read)

func LeaseStateToString

func LeaseStateToString(state uint32) string

LeaseStateToString converts a lease state value to a human-readable string.

func NewDeadlockError

func NewDeadlockError(waiter string, blockedBy []string) *errors.StoreError

NewDeadlockError creates error for deadlock detection.

func NewGracePeriodError

func NewGracePeriodError(remainingSeconds int) *errors.StoreError

NewGracePeriodError creates error for grace period blocking.

func NewLockConflictError

func NewLockConflictError(path string, conflict *UnifiedLockConflict) *errors.StoreError

NewLockConflictError creates error for unified lock conflicts.

func NewLockLimitExceededError

func NewLockLimitExceededError(limitType string, current, max int) *errors.StoreError

NewLockLimitExceededError creates error for lock limit violations.

func NewLockNotFoundError

func NewLockNotFoundError(path string) *errors.StoreError

NewLockNotFoundError creates error for missing locks.

func NewLockedError

func NewLockedError(path string, conflict *LockConflict) *errors.StoreError

NewLockedError creates error for lock conflicts (legacy FileLock).

func OpLocksConflict

func OpLocksConflict(existing, requested *OpLock) bool

OpLocksConflict checks if two leases on the same file conflict.

Conflict rules:

  • Same LeaseKey = no conflict (same client caching unit)
  • Different keys with overlapping exclusive states (W) = conflict
  • Multiple Read leases from different clients = no conflict
  • Write lease requires exclusive access = conflicts with other leases
  • Handle lease without Read/Write = no data conflict

Returns true if the leases conflict and one must be broken.

func RangesOverlap

func RangesOverlap(offset1, length1, offset2, length2 uint64) bool

RangesOverlap returns true if two byte ranges overlap. Length of 0 means "to end of file" (unbounded).

func TranslateNFSConflictReason

func TranslateNFSConflictReason(lease *UnifiedLock) string

TranslateNFSConflictReason generates a human-readable reason for NFS denial due to an SMB lease conflict.

This is used for INFO-level logging when an NFS operation is denied due to an existing SMB lease.

Parameters:

  • lease: The conflicting SMB lease

Returns:

  • string: Human-readable conflict reason for logging

Example output:

"SMB client 'client1' holds Write lease (RW)"
"SMB client 'client1' holds Handle lease (RH)"

func TranslateSMBConflictReason

func TranslateSMBConflictReason(lock *UnifiedLock) string

TranslateSMBConflictReason generates a human-readable reason for SMB denial due to an NLM lock conflict.

This is used for INFO-level logging when an SMB operation is denied due to an existing NLM byte-range lock. Per CONTEXT.md, cross-protocol conflicts are logged at INFO level since they're working as designed.

Parameters:

  • lock: The conflicting NLM byte-range lock

Returns:

  • string: Human-readable conflict reason for logging

Example output:

"NFS client 'host1' holds exclusive lock on bytes 0-1024"
"NFS client 'host1' holds shared lock on entire file"

Types

type AccessMode

type AccessMode int

AccessMode represents SMB share mode reservations. These control what other clients can do while the file is open. NFS protocols ignore this field.

const (
	// AccessModeNone allows all operations by other clients (default).
	AccessModeNone AccessMode = iota

	// AccessModeDenyRead prevents other clients from reading.
	AccessModeDenyRead

	// AccessModeDenyWrite prevents other clients from writing.
	AccessModeDenyWrite

	// AccessModeDenyAll prevents other clients from reading or writing.
	AccessModeDenyAll
)

func (AccessMode) String

func (sr AccessMode) String() string

String returns a human-readable name for the share reservation.

type BreakCallbacks

type BreakCallbacks interface {
	// OnOpLockBreak is called when an oplock/lease must be broken.
	//
	// Parameters:
	//   - handleKey: The file handle key for the affected file
	//   - lock: The lock whose oplock must be broken
	//   - breakToState: The target lease state after break (e.g., LeaseStateRead or LeaseStateNone)
	OnOpLockBreak(handleKey string, lock *UnifiedLock, breakToState uint32)

	// OnByteRangeRevoke is called when a byte-range lock must be revoked
	// due to a cross-protocol conflict.
	//
	// Parameters:
	//   - handleKey: The file handle key for the affected file
	//   - lock: The byte-range lock that conflicts
	//   - reason: Human-readable reason for the revocation
	OnByteRangeRevoke(handleKey string, lock *UnifiedLock, reason string)

	// OnAccessConflict is called when an SMB access mode conflict is detected.
	//
	// Parameters:
	//   - handleKey: The file handle key for the affected file
	//   - existingLock: The lock holding the conflicting access mode
	//   - requestedMode: The access mode that was requested
	OnAccessConflict(handleKey string, existingLock *UnifiedLock, requestedMode AccessMode)
}

BreakCallbacks provides typed callback methods for cross-protocol coordination.

Protocol adapters register implementations to receive notifications when lock breaks are required. Each method corresponds to a different break type:

  • OnOpLockBreak: OpLock/lease must be broken (e.g., NFS delegation recall)
  • OnByteRangeRevoke: Byte-range lock must be revoked
  • OnAccessConflict: Access mode conflict detected

NFS adapter typically only registers OnOpLockBreak (for delegation recall). SMB adapter registers all three callbacks.

Callbacks are invoked synchronously during lock operations. Implementations should be lightweight or offload heavy work to background goroutines.

type ClientRegistration

type ClientRegistration struct {
	// ClientID is the unique client identifier.
	ClientID string

	// AdapterType identifies which protocol adapter (e.g., "nfs", "smb").
	AdapterType string

	// TTL is how long to keep locks after disconnect (0 = immediate release).
	TTL time.Duration

	// RegisteredAt is when the client registered.
	RegisteredAt time.Time

	// LastSeen is the last activity timestamp.
	LastSeen time.Time

	// RemoteAddr is the client IP address (for logging/metrics).
	RemoteAddr string

	// LockCount is the number of locks held by this client.
	LockCount int

	// MonName is the monitored hostname (mon_id.mon_name from SM_MON).
	// This is typically the server's hostname that the client is monitoring.
	MonName string

	// Priv is the 16-byte private data returned in SM_NOTIFY callbacks.
	// Stored from SM_MON and sent back to the client when state changes.
	Priv [16]byte

	// SMState is the client's NSM state counter at registration time.
	// Used to detect stale registrations after client restarts.
	SMState int32

	// CallbackInfo contains RPC callback details from SM_MON my_id field.
	// Used to send SM_NOTIFY callbacks when server restarts or client crashes.
	CallbackInfo *NSMCallback
}

ClientRegistration represents a registered client connection.

func FromPersistedClientRegistration

func FromPersistedClientRegistration(persisted *PersistedClientRegistration) *ClientRegistration

FromPersistedClientRegistration converts a persisted registration back to ClientRegistration. This is used when loading registrations from the store.

type ClientRegistrationStore

type ClientRegistrationStore interface {
	// PutClientRegistration stores or updates a client registration.
	// If a registration with the same ClientID exists, it is replaced.
	PutClientRegistration(ctx context.Context, reg *PersistedClientRegistration) error

	// GetClientRegistration retrieves a registration by client ID.
	// Returns nil, nil if the registration does not exist.
	GetClientRegistration(ctx context.Context, clientID string) (*PersistedClientRegistration, error)

	// DeleteClientRegistration removes a registration.
	// Returns nil if the registration does not exist.
	DeleteClientRegistration(ctx context.Context, clientID string) error

	// ListClientRegistrations returns all stored registrations.
	// Used on server startup to send SM_NOTIFY callbacks.
	ListClientRegistrations(ctx context.Context) ([]*PersistedClientRegistration, error)

	// DeleteAllClientRegistrations removes all registrations.
	// Used for SM_UNMON_ALL to clear all monitoring for a client.
	// Returns the count of deleted registrations.
	DeleteAllClientRegistrations(ctx context.Context) (int, error)

	// DeleteClientRegistrationsByMonName removes all registrations monitoring a specific host.
	// Used when a monitored host is known to have crashed.
	// Returns the count of deleted registrations.
	DeleteClientRegistrationsByMonName(ctx context.Context, monName string) (int, error)
}

ClientRegistrationStore provides persistence for NSM client registrations. Implementations exist in memory, badger, and postgres stores.

This interface enables crash recovery: 1. On server startup, load all registrations from previous run 2. Increment server epoch (state counter) 3. Send SM_NOTIFY to all registered callbacks with new state 4. Clients can then reclaim locks during grace period

type Config

type Config struct {
	// MaxLocksPerFile is the maximum number of locks allowed on a single file.
	// Prevents a single file from exhausting lock table resources.
	// Default: 1000
	MaxLocksPerFile int `mapstructure:"max_locks_per_file" yaml:"max_locks_per_file"`

	// MaxLocksPerClient is the maximum number of locks a single client can hold.
	// Prevents a single client from exhausting lock table resources.
	// Default: 10000
	MaxLocksPerClient int `mapstructure:"max_locks_per_client" yaml:"max_locks_per_client"`

	// MaxTotalLocks is the maximum total locks across all files and clients.
	// Provides a hard ceiling on lock manager memory usage.
	// Default: 100000
	MaxTotalLocks int `mapstructure:"max_total_locks" yaml:"max_total_locks"`

	// BlockingTimeout is the server-side timeout for blocking lock requests.
	// After this duration, the server will return NLM_LCK_DENIED_NOLOCKS or
	// similar to the client. The client may retry.
	// Default: 60s
	BlockingTimeout time.Duration `mapstructure:"blocking_timeout" yaml:"blocking_timeout"`

	// GracePeriodDuration is the duration of the grace period after server restart.
	// During this time, only lock reclaims are allowed (new locks are denied).
	// Default: 90s (NFS spec recommends 90 seconds minimum)
	GracePeriodDuration time.Duration `mapstructure:"grace_period" yaml:"grace_period"`

	// MandatoryLocking controls whether locks are mandatory or advisory.
	// - false (default): Advisory locks - the lock manager tracks locks but
	//   I/O operations are not blocked by locks (only lock operations check locks)
	// - true: Mandatory locks - I/O operations check and are blocked by locks
	// Advisory locking is more common and has better performance.
	// Default: false
	MandatoryLocking bool `mapstructure:"mandatory_locking" yaml:"mandatory_locking"`
}

Config contains configuration settings for the lock manager.

These settings control lock limits, timeouts, and behavior across all protocols (NLM, SMB, NFSv4).

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns a Config with sensible defaults.

These defaults are based on common production deployments and NFS/SMB protocol recommendations.

type ConnectionTracker

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

ConnectionTracker manages client connections for lock lifecycle.

Thread Safety: All operations are protected by a read-write mutex.

func NewConnectionTracker

func NewConnectionTracker(config ConnectionTrackerConfig) *ConnectionTracker

NewConnectionTracker creates a new connection tracker.

func (*ConnectionTracker) CancelDisconnect

func (ct *ConnectionTracker) CancelDisconnect(clientID string) bool

CancelDisconnect cancels a pending disconnect timer (client reconnected).

func (*ConnectionTracker) ClearNSMInfo

func (ct *ConnectionTracker) ClearNSMInfo(clientID string)

ClearNSMInfo removes NSM-specific fields for a client. Called after SM_UNMON to clear monitoring registration.

func (*ConnectionTracker) Close

func (ct *ConnectionTracker) Close()

Close cancels all pending disconnect timers and clears state.

func (*ConnectionTracker) DecrementLockCount

func (ct *ConnectionTracker) DecrementLockCount(clientID string)

DecrementLockCount decrements the lock count for a client.

func (*ConnectionTracker) GetClient

func (ct *ConnectionTracker) GetClient(clientID string) (*ClientRegistration, bool)

GetClient returns the registration for a client if it exists.

func (*ConnectionTracker) GetClientCount

func (ct *ConnectionTracker) GetClientCount(adapterType string) int

GetClientCount returns the number of clients, optionally filtered by adapter. Pass empty string for adapterType to get total count.

func (*ConnectionTracker) GetNSMClients

func (ct *ConnectionTracker) GetNSMClients() []*ClientRegistration

GetNSMClients returns all clients with NSM callback info (for SM_NOTIFY). Returns copies to prevent modification of internal state.

func (*ConnectionTracker) GetPendingDisconnectCount

func (ct *ConnectionTracker) GetPendingDisconnectCount() int

GetPendingDisconnectCount returns the number of pending disconnect timers.

func (*ConnectionTracker) IncrementLockCount

func (ct *ConnectionTracker) IncrementLockCount(clientID string)

IncrementLockCount increments the lock count for a client.

func (*ConnectionTracker) ListClients

func (ct *ConnectionTracker) ListClients(adapterType string) []*ClientRegistration

ListClients returns all clients, optionally filtered by adapter type. Pass empty string for adapterType to get all clients.

func (*ConnectionTracker) RegisterClient

func (ct *ConnectionTracker) RegisterClient(clientID, adapterType, remoteAddr string, ttl time.Duration) error

RegisterClient registers a new client or updates an existing one. Returns ErrConnectionLimitReached if the limit is exceeded.

func (*ConnectionTracker) UnregisterClient

func (ct *ConnectionTracker) UnregisterClient(clientID string)

UnregisterClient removes a client, potentially with delayed lock cleanup.

func (*ConnectionTracker) UpdateLastSeen

func (ct *ConnectionTracker) UpdateLastSeen(clientID string)

UpdateLastSeen updates the last activity timestamp for a client.

func (*ConnectionTracker) UpdateNSMInfo

func (ct *ConnectionTracker) UpdateNSMInfo(clientID, monName string, priv [16]byte, callback *NSMCallback)

UpdateNSMInfo updates NSM-specific fields for a client. Called after SM_MON to store monitoring callback details.

func (*ConnectionTracker) UpdateSMState

func (ct *ConnectionTracker) UpdateSMState(clientID string, state int32)

UpdateSMState updates the NSM state counter for a client.

type ConnectionTrackerConfig

type ConnectionTrackerConfig struct {
	// MaxConnectionsPerAdapter limits connections by adapter type.
	MaxConnectionsPerAdapter map[string]int

	// DefaultMaxConnections is the fallback limit (default: 10000).
	DefaultMaxConnections int

	// StaleCheckInterval is how often to check for stale clients (default: 30s).
	StaleCheckInterval time.Duration

	// OnClientDisconnect is called when a client is fully disconnected.
	OnClientDisconnect func(clientID string)
}

ConnectionTrackerConfig configures the connection tracker.

func DefaultConnectionTrackerConfig

func DefaultConnectionTrackerConfig() ConnectionTrackerConfig

DefaultConnectionTrackerConfig returns a config with sensible defaults.

type FileHandle

type FileHandle string

FileHandle represents an opaque file handle. This is defined here to avoid circular imports with the metadata package. The metadata package also defines FileHandle as []byte.

type FileLock

type FileLock struct {
	// ID is the lock identifier from the client.
	// For SMB2: derived from lock request (often 0 for simple locks)
	// For NLM: opaque client-provided lock handle
	ID uint64

	// SessionID identifies who holds the lock.
	// For SMB2: SessionID from SMB header
	// For NLM: hash of network address + client PID
	SessionID uint64

	// Offset is the starting byte offset of the lock.
	Offset uint64

	// Length is the number of bytes locked.
	// 0 means "to end of file" (unbounded).
	Length uint64

	// Exclusive indicates lock type.
	// true = exclusive (write lock, blocks all other locks)
	// false = shared (read lock, allows other shared locks)
	Exclusive bool

	// AcquiredAt is the time the lock was acquired.
	AcquiredAt time.Time

	// ClientAddr is the network address of the client holding the lock.
	// Used for debugging and logging.
	ClientAddr string
}

FileLock represents a byte-range lock on a file.

Byte-range locks control what portions of a file can be read/written while locked by other clients. They are used by SMB2 LOCK command and NFS NLM protocol.

Lock Types:

  • Exclusive (write): No other locks allowed on overlapping range
  • Shared (read): Multiple shared locks allowed, no exclusive locks

Lock Lifetime: Locks are advisory and ephemeral (in-memory only). They persist until:

  • Explicitly released via UnlockFile
  • File is closed (UnlockAllForSession)
  • Session disconnects (cleanup all session locks)
  • Server restarts (all locks lost)

type GracePeriodManager

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

GracePeriodManager manages the grace period state machine.

After server restart, clients need time to reclaim their locks. The grace period allows reclaims while blocking new lock requests. This prevents races where a new client could acquire a lock before the previous owner has a chance to reclaim it.

The grace period ends when:

  • The configured duration expires (timer-based exit)
  • All expected clients have reclaimed their locks (early exit)
  • ExitGracePeriod() is explicitly called

Thread Safety: All methods are safe for concurrent use by multiple goroutines.

func NewGracePeriodManager

func NewGracePeriodManager(duration time.Duration, onGraceEnd func()) *GracePeriodManager

NewGracePeriodManager creates a new GracePeriodManager.

Parameters:

  • duration: The grace period duration (how long to wait for reclaims)
  • onGraceEnd: Callback invoked when grace period ends (can be nil). This is typically used to clean up unclaimed locks.

The manager starts in Normal state.

func (*GracePeriodManager) Close

func (gpm *GracePeriodManager) Close()

Close stops the grace period manager and cleans up resources.

This cancels any pending timer but does NOT call the onGraceEnd callback.

func (*GracePeriodManager) EnterGracePeriod

func (gpm *GracePeriodManager) EnterGracePeriod(expectedClients []string)

EnterGracePeriod transitions to the Active state.

This is called on server startup when persisted locks exist. During the grace period:

  • Reclaim operations are allowed
  • Lock test operations are allowed
  • New lock requests are denied with ErrGracePeriod

Parameters:

  • expectedClients: List of client IDs expected to reclaim their locks. If all these clients reclaim, the grace period exits early.

If already in Active state, this is a no-op.

func (*GracePeriodManager) ExitGracePeriod

func (gpm *GracePeriodManager) ExitGracePeriod()

ExitGracePeriod manually exits the grace period.

This transitions to Normal state, cancels any pending timer, and invokes the onGraceEnd callback.

If already in Normal state, this is a no-op.

func (*GracePeriodManager) GetDuration

func (gpm *GracePeriodManager) GetDuration() time.Duration

GetDuration returns the configured grace period duration.

func (*GracePeriodManager) GetExpectedClients

func (gpm *GracePeriodManager) GetExpectedClients() []string

GetExpectedClients returns the list of expected client IDs.

This is useful for debugging and monitoring.

func (*GracePeriodManager) GetReclaimedClients

func (gpm *GracePeriodManager) GetReclaimedClients() []string

GetReclaimedClients returns the list of clients that have reclaimed.

This is useful for debugging and monitoring.

func (*GracePeriodManager) GetRemainingTime

func (gpm *GracePeriodManager) GetRemainingTime() time.Duration

GetRemainingTime returns the time remaining until the grace period ends.

Returns 0 if not in grace period or if the grace period has expired.

func (*GracePeriodManager) GetState

func (gpm *GracePeriodManager) GetState() GraceState

GetState returns the current grace period state.

func (*GracePeriodManager) IsOperationAllowed

func (gpm *GracePeriodManager) IsOperationAllowed(op Operation) (bool, error)

IsOperationAllowed checks if a lock operation is allowed in the current state.

Parameters:

  • op: The lock operation to check

Returns:

  • (true, nil) if the operation is allowed
  • (false, ErrGracePeriod error) if the operation is blocked

During Normal state: all operations allowed. During Active state:

  • Reclaims: allowed
  • Tests: allowed
  • New locks: denied with ErrGracePeriod

func (*GracePeriodManager) MarkReclaimed

func (gpm *GracePeriodManager) MarkReclaimed(clientID string)

MarkReclaimed records that a client has reclaimed their locks.

If all expected clients have reclaimed, the grace period exits early.

Parameters:

  • clientID: The client ID that has reclaimed

type GraceState

type GraceState int

GraceState represents the state of the grace period.

const (
	// GraceStateNormal indicates normal operation - all lock operations allowed.
	GraceStateNormal GraceState = iota

	// GraceStateActive indicates grace period is active - only reclaims allowed.
	GraceStateActive
)

func (GraceState) String

func (gs GraceState) String() string

String returns a human-readable name for the grace state.

type Limits

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

Limits tracks current lock usage for limit enforcement.

Thread Safety: Limits is safe for concurrent use by multiple goroutines.

func NewLimits

func NewLimits() *Limits

NewLimits creates a new Limits tracker.

func (*Limits) CheckLimits

func (ll *Limits) CheckLimits(config Config, fileHandle, clientID string) error

CheckLimits verifies that acquiring a new lock would not exceed any limits.

Parameters:

  • config: The lock configuration containing limits
  • fileHandle: The file handle for the lock
  • clientID: The client ID acquiring the lock

Returns:

  • nil if limits would not be exceeded
  • ErrLockLimitExceeded if any limit would be exceeded

func (*Limits) DecrementCounts

func (ll *Limits) DecrementCounts(fileHandle, clientID string)

DecrementCounts updates counters after releasing a lock.

Call this AFTER the lock has been successfully released.

Parameters:

  • fileHandle: The file handle the lock was released from
  • clientID: The client ID that released the lock

func (*Limits) DecrementCountsN

func (ll *Limits) DecrementCountsN(fileHandle, clientID string, count int)

DecrementCountsN updates counters after releasing multiple locks at once.

Call this when releasing all locks for a file or all locks for a client.

Parameters:

  • fileHandle: The file handle (can be empty if not file-specific)
  • clientID: The client ID (can be empty if not client-specific)
  • count: Number of locks being released

func (*Limits) GetClientCount

func (ll *Limits) GetClientCount(clientID string) int

GetClientCount returns the current lock count for a specific client.

func (*Limits) GetFileCount

func (ll *Limits) GetFileCount(fileHandle string) int

GetFileCount returns the current lock count for a specific file.

func (*Limits) GetStats

func (ll *Limits) GetStats() Stats

GetStats returns current lock usage statistics.

This is useful for monitoring and debugging.

func (*Limits) GetTotalCount

func (ll *Limits) GetTotalCount() int

GetTotalCount returns the current total lock count.

func (*Limits) IncrementCounts

func (ll *Limits) IncrementCounts(fileHandle, clientID string)

IncrementCounts updates counters after successfully acquiring a lock.

Call this AFTER the lock has been successfully acquired.

Parameters:

  • fileHandle: The file handle the lock was acquired on
  • clientID: The client ID that acquired the lock

func (*Limits) Reset

func (ll *Limits) Reset()

Reset clears all lock counts (useful for testing).

type LockConflict

type LockConflict struct {
	// Offset is the starting byte offset of the conflicting lock.
	Offset uint64

	// Length is the number of bytes of the conflicting lock.
	Length uint64

	// Exclusive indicates type of conflicting lock.
	Exclusive bool

	// OwnerSessionID identifies the client holding the conflicting lock.
	OwnerSessionID uint64
}

LockConflict describes a conflicting lock for error reporting.

When LockFile or TestLock fails due to a conflict, this structure provides information about the conflicting lock. This can be used by protocols to report conflict details back to clients.

type LockManager

type LockManager interface {

	// AddUnifiedLock adds a unified lock (byte-range or oplock).
	// Returns error if the lock conflicts with existing locks.
	AddUnifiedLock(handleKey string, lock *UnifiedLock) error

	// RemoveUnifiedLock removes a unified lock using POSIX splitting semantics.
	RemoveUnifiedLock(handleKey string, owner LockOwner, offset, length uint64) error

	// ListUnifiedLocks returns all unified locks on a file.
	ListUnifiedLocks(handleKey string) []*UnifiedLock

	// RemoveFileUnifiedLocks removes all unified locks for a file.
	RemoveFileUnifiedLocks(handleKey string)

	// UpgradeLock atomically converts a shared lock to exclusive if no other readers exist.
	UpgradeLock(handleKey string, owner LockOwner, offset, length uint64) (*UnifiedLock, error)

	// GetUnifiedLock retrieves a specific unified lock by owner and range.
	GetUnifiedLock(handleKey string, owner LockOwner, offset, length uint64) (*UnifiedLock, error)

	// CheckAndBreakOpLocksForWrite checks and breaks oplocks that conflict with a write.
	// Write breaks all Write oplocks to None, Read oplocks to None.
	// excludeOwner can be nil to check all owners.
	CheckAndBreakOpLocksForWrite(handleKey string, excludeOwner *LockOwner) error

	// CheckAndBreakOpLocksForRead checks and breaks oplocks that conflict with a read.
	// Read only breaks Write oplocks (to Read).
	// excludeOwner can be nil to check all owners.
	CheckAndBreakOpLocksForRead(handleKey string, excludeOwner *LockOwner) error

	// CheckAndBreakOpLocksForDelete checks and breaks all oplocks on a file.
	// Delete breaks all oplocks to None.
	// excludeOwner can be nil to check all owners.
	CheckAndBreakOpLocksForDelete(handleKey string, excludeOwner *LockOwner) error

	// Lock attempts to acquire a byte-range lock on a file.
	Lock(handleKey string, lock FileLock) error

	// Unlock releases a specific byte-range lock.
	Unlock(handleKey string, sessionID uint64, offset, length uint64) error

	// TestLock checks if a lock would succeed without acquiring it.
	TestLock(handleKey string, lock FileLock) (*LockConflict, error)

	// ListLocks returns all active byte-range locks on a file.
	ListLocks(handleKey string) []FileLock

	// EnterGracePeriod transitions to grace period state.
	EnterGracePeriod(expectedClients []string)

	// ExitGracePeriod manually exits the grace period.
	ExitGracePeriod()

	// IsOperationAllowed checks if a lock operation is allowed in the current state.
	IsOperationAllowed(op Operation) (bool, error)

	// MarkReclaimed records that a client has reclaimed their locks.
	MarkReclaimed(clientID string)

	// IsInGracePeriod returns true if grace period is currently active.
	IsInGracePeriod() bool

	// RegisterBreakCallbacks registers typed callbacks for break notifications.
	RegisterBreakCallbacks(callbacks BreakCallbacks)

	// RemoveAllLocks removes all locks (both legacy and unified) for a file.
	RemoveAllLocks(handleKey string)

	// RemoveClientLocks removes all locks held by a specific client.
	RemoveClientLocks(clientID string)

	// GetStats returns current lock manager statistics.
	GetStats() ManagerStats
}

LockManager provides unified lock management for all protocols.

This is the single interface that both NFS and SMB adapters use for lock operations. It unifies byte-range locks, oplocks/leases, grace period management, and break callback registration into a single coherent API.

The interface covers:

  • Unified lock CRUD (AddUnifiedLock, RemoveUnifiedLock, etc.)
  • Centralized break operations (replaces OplockChecker global)
  • Legacy byte-range locks (backward compat for existing callers)
  • Grace period management
  • Break callback registration
  • Connection/cleanup operations

type LockOwner

type LockOwner struct {
	// OwnerID is the protocol-provided owner identifier.
	// Format: "{protocol}:{details}" - treated as OPAQUE by lock manager.
	// The lock manager never parses this string; it only compares for equality.
	OwnerID string

	// ClientID is the connection tracker client ID.
	// Used to clean up locks when a client disconnects.
	ClientID string

	// ShareName is the share this lock belongs to.
	// Used for per-share lock tracking and cleanup.
	ShareName string
}

LockOwner identifies the owner of a lock in a protocol-agnostic way.

The OwnerID field is an opaque string that the lock manager does NOT parse. Different protocols encode their identity information differently:

  • NLM: "nlm:client1:pid123"
  • SMB: "smb:session456:pid789"
  • NFSv4: "nfs4:clientid:stateid"

This enables cross-protocol lock conflict detection (LOCK-04): if an NLM client and SMB client both request exclusive locks on the same range, they will correctly conflict because the OwnerIDs are different.

type LockQuery

type LockQuery struct {
	// FileID filters by file (string representation of FileHandle).
	// Empty string means no file filtering.
	FileID string

	// OwnerID filters by lock owner.
	// Empty string means no owner filtering.
	OwnerID string

	// ClientID filters by client.
	// Empty string means no client filtering.
	ClientID string

	// ShareName filters by share.
	// Empty string means no share filtering.
	ShareName string

	// IsLease filters by lock type.
	// nil means no type filtering (both leases and byte-range locks).
	// true means leases only.
	// false means byte-range locks only.
	IsLease *bool
}

LockQuery specifies filters for listing locks.

All fields are optional. Empty fields are not used in filtering. Multiple fields are ANDed together.

func (LockQuery) IsEmpty

func (q LockQuery) IsEmpty() bool

IsEmpty returns true if the query has no filters.

func (LockQuery) MatchesLock

func (q LockQuery) MatchesLock(lk *PersistedLock) bool

MatchesLock returns true if the lock matches all query filters. Used by store implementations for consistent filtering logic.

type LockResult

type LockResult struct {
	// Success indicates whether the lock was acquired.
	Success bool

	// Lock is the acquired lock (nil if !Success).
	Lock *UnifiedLock

	// Conflict is the conflicting lock information (nil if Success).
	Conflict *UnifiedLockConflict

	// ShouldWait indicates whether the caller should wait and retry.
	// True when a blocking request found a conflict.
	ShouldWait bool

	// WaitFor is the list of owner IDs to wait for (for deadlock detection).
	WaitFor []string
}

LockResult represents the result of a lock operation.

type LockStore

type LockStore interface {

	// PutLock persists a lock. Overwrites if lock with same ID exists.
	PutLock(ctx context.Context, lock *PersistedLock) error

	// GetLock retrieves a lock by ID.
	// Returns ErrLockNotFound if lock doesn't exist.
	GetLock(ctx context.Context, lockID string) (*PersistedLock, error)

	// DeleteLock removes a lock by ID.
	// Returns ErrLockNotFound if lock doesn't exist.
	DeleteLock(ctx context.Context, lockID string) error

	// ListLocks returns locks matching the query.
	// Empty query returns all locks.
	ListLocks(ctx context.Context, query LockQuery) ([]*PersistedLock, error)

	// DeleteLocksByClient removes all locks for a client.
	// Returns number of locks deleted.
	// Used when a client disconnects to clean up its locks.
	DeleteLocksByClient(ctx context.Context, clientID string) (int, error)

	// DeleteLocksByFile removes all locks for a file.
	// Returns number of locks deleted.
	// Used when a file is deleted.
	DeleteLocksByFile(ctx context.Context, fileID string) (int, error)

	// GetServerEpoch returns current server epoch.
	// Returns 0 for a fresh server (never started).
	GetServerEpoch(ctx context.Context) (uint64, error)

	// IncrementServerEpoch increments and returns new epoch.
	// Called during server startup to detect restarts.
	// Locks with epoch < current epoch are stale.
	IncrementServerEpoch(ctx context.Context) (uint64, error)

	// ReclaimLease reclaims an existing lease during grace period.
	// This validates the lease existed in persistent storage before restart
	// and allows the client to re-establish the lease state.
	//
	// Parameters:
	//   - ctx: Context for cancellation
	//   - fileHandle: File handle for the lease
	//   - leaseKey: The 16-byte SMB lease key
	//   - clientID: Client identifier for ownership verification
	//
	// Returns:
	//   - *UnifiedLock: The reclaimed lease on success
	//   - error: ErrLockNotFound if lease doesn't exist
	ReclaimLease(ctx context.Context, fileHandle FileHandle, leaseKey [16]byte, clientID string) (*UnifiedLock, error)
}

LockStore defines operations for persisting locks to the metadata store.

This interface enables lock state to survive server restarts, supporting:

  • NLM/SMB grace period for lock reclamation
  • Split-brain detection via server epochs
  • Client disconnect cleanup

Thread Safety: Implementations must be safe for concurrent use by multiple goroutines. Operations within a transaction (via Transaction interface) share the transaction's isolation level.

type LockType

type LockType int

LockType represents the type of lock (shared or exclusive).

const (
	// LockTypeShared is a shared (read) lock - multiple readers allowed.
	LockTypeShared LockType = iota

	// LockTypeExclusive is an exclusive (write) lock - no other locks allowed.
	LockTypeExclusive
)

func (LockType) String

func (lt LockType) String() string

String returns a human-readable name for the lock type.

type Manager

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

Manager manages byte-range file locks for SMB/NLM protocols.

This is a shared, in-memory implementation that can be embedded in any metadata store. Locks are ephemeral and lost on server restart.

Manager implements the LockManager interface, providing unified lock management including byte-range locks, oplocks, grace period, and typed break callbacks.

Thread Safety: Manager is safe for concurrent use by multiple goroutines.

func NewManager

func NewManager() *Manager

NewManager creates a new lock manager.

func NewManagerWithGracePeriod

func NewManagerWithGracePeriod(gracePeriod *GracePeriodManager) *Manager

NewManagerWithGracePeriod creates a new lock manager with a grace period manager.

func (*Manager) AddUnifiedLock

func (lm *Manager) AddUnifiedLock(handleKey string, lock *UnifiedLock) error

AddUnifiedLock adds a unified lock to the storage.

Checks for conflicts using the ConflictsWith method which handles all 4 conflict cases: access modes, oplock-oplock, oplock-byterange, byterange-byterange.

func (*Manager) CheckAndBreakOpLocksForDelete

func (lm *Manager) CheckAndBreakOpLocksForDelete(handleKey string, excludeOwner *LockOwner) error

CheckAndBreakOpLocksForDelete checks and initiates breaks for all oplocks on a file being deleted.

Delete operations break all non-None oplocks to None.

func (*Manager) CheckAndBreakOpLocksForRead

func (lm *Manager) CheckAndBreakOpLocksForRead(handleKey string, excludeOwner *LockOwner) error

CheckAndBreakOpLocksForRead checks and initiates breaks for oplocks that conflict with a read operation.

Read operations only break Write oplocks (downgraded to Read).

func (*Manager) CheckAndBreakOpLocksForWrite

func (lm *Manager) CheckAndBreakOpLocksForWrite(handleKey string, excludeOwner *LockOwner) error

CheckAndBreakOpLocksForWrite checks and initiates breaks for oplocks that conflict with a write operation.

Write operations break all oplocks with Read or Write state to None.

func (*Manager) CheckForIO

func (lm *Manager) CheckForIO(handleKey string, sessionID, offset, length uint64, isWrite bool) *LockConflict

CheckForIO checks if an I/O operation would conflict with existing locks.

Returns nil if I/O is allowed, or conflict details if blocked.

func (*Manager) EnterGracePeriod

func (lm *Manager) EnterGracePeriod(expectedClients []string)

EnterGracePeriod transitions to grace period state. If no grace period manager is configured, this is a no-op.

func (*Manager) ExitGracePeriod

func (lm *Manager) ExitGracePeriod()

ExitGracePeriod manually exits the grace period. If no grace period manager is configured, this is a no-op.

func (*Manager) GetStats

func (lm *Manager) GetStats() ManagerStats

GetStats returns current lock manager statistics.

func (*Manager) GetUnifiedLock

func (lm *Manager) GetUnifiedLock(handleKey string, owner LockOwner, offset, length uint64) (*UnifiedLock, error)

GetUnifiedLock retrieves a specific unified lock by owner and range.

Returns the matching lock or ErrLockNotFound if no matching lock exists.

func (*Manager) IsInGracePeriod

func (lm *Manager) IsInGracePeriod() bool

IsInGracePeriod returns true if grace period is currently active.

func (*Manager) IsOperationAllowed

func (lm *Manager) IsOperationAllowed(op Operation) (bool, error)

IsOperationAllowed checks if a lock operation is allowed in the current state. If no grace period manager is configured, all operations are allowed.

func (*Manager) ListLocks

func (lm *Manager) ListLocks(handleKey string) []FileLock

ListLocks returns all active locks on a file.

Returns nil if no locks exist.

func (*Manager) ListUnifiedLocks

func (lm *Manager) ListUnifiedLocks(handleKey string) []*UnifiedLock

ListUnifiedLocks returns all unified locks on a file.

func (*Manager) Lock

func (lm *Manager) Lock(handleKey string, lock FileLock) error

Lock attempts to acquire a byte-range lock on a file.

This is a low-level CRUD operation with no permission checking. Business logic (permission checks, file type validation) should be performed by the caller.

Returns nil on success, or ErrLocked if a conflict exists.

func (*Manager) MarkReclaimed

func (lm *Manager) MarkReclaimed(clientID string)

MarkReclaimed records that a client has reclaimed their locks. If no grace period manager is configured, this is a no-op.

func (*Manager) RegisterBreakCallbacks

func (lm *Manager) RegisterBreakCallbacks(callbacks BreakCallbacks)

RegisterBreakCallbacks registers typed callbacks for break notifications.

Multiple callbacks can be registered (one per protocol adapter). Callbacks are invoked in registration order during break operations.

func (*Manager) RemoveAllLocks

func (lm *Manager) RemoveAllLocks(handleKey string)

RemoveAllLocks removes all locks (both legacy and unified) for a file.

func (*Manager) RemoveClientLocks

func (lm *Manager) RemoveClientLocks(clientID string)

RemoveClientLocks removes all locks held by a specific client.

This iterates all files and removes any unified locks owned by the specified client ID. Also removes legacy locks by scanning all sessions.

func (*Manager) RemoveFileLocks

func (lm *Manager) RemoveFileLocks(handleKey string)

RemoveFileLocks removes all locks for a file.

Called when a file is deleted to clean up any stale lock entries.

func (*Manager) RemoveFileUnifiedLocks

func (lm *Manager) RemoveFileUnifiedLocks(handleKey string)

RemoveFileUnifiedLocks removes all unified locks for a file.

func (*Manager) RemoveUnifiedLock

func (lm *Manager) RemoveUnifiedLock(handleKey string, owner LockOwner, offset, length uint64) error

RemoveUnifiedLock removes a unified lock using POSIX splitting semantics.

func (*Manager) TestLock

func (lm *Manager) TestLock(handleKey string, lock FileLock) (*LockConflict, error)

TestLock checks if a lock would succeed without acquiring it.

Returns (*LockConflict, nil) if conflict exists, or (nil, nil) if lock would succeed.

func (*Manager) TestLockByParams

func (lm *Manager) TestLockByParams(handleKey string, sessionID, offset, length uint64, exclusive bool) (bool, *LockConflict)

TestLockByParams checks if a lock would succeed without acquiring it (legacy params).

Returns (true, nil) if lock would succeed, (false, conflict) if conflict exists.

func (*Manager) Unlock

func (lm *Manager) Unlock(handleKey string, sessionID, offset, length uint64) error

Unlock releases a specific byte-range lock.

The lock is identified by session, offset, and length - all must match exactly.

Returns nil on success, or ErrLockNotFound if the lock wasn't found.

func (*Manager) UnlockAllForSession

func (lm *Manager) UnlockAllForSession(handleKey string, sessionID uint64) int

UnlockAllForSession releases all locks held by a session on a file.

Returns the number of locks released.

func (*Manager) UpgradeLock

func (lm *Manager) UpgradeLock(handleKey string, owner LockOwner, offset, length uint64) (*UnifiedLock, error)

UpgradeLock atomically converts a shared lock to exclusive if no other readers exist.

This implements the user decision: "Lock upgrade: Atomic upgrade supported (read -> write if no other readers)".

Steps:

  1. Find existing shared lock owned by `owner` covering the range
  2. Check if any OTHER owners hold shared locks on overlapping range
  3. If other readers exist: return ErrLockConflict
  4. If no other readers: atomically change lock type to Exclusive

Parameters:

  • handleKey: The file handle key
  • owner: The lock owner requesting the upgrade
  • offset: Starting byte offset of the range to upgrade
  • length: Number of bytes (0 = to EOF)

Returns:

  • *UnifiedLock: The upgraded lock on success
  • error: ErrLockConflict if other readers exist, ErrLockNotFound if no lock to upgrade

type ManagerStats

type ManagerStats struct {
	// TotalLegacyLocks is the total number of legacy byte-range locks.
	TotalLegacyLocks int

	// TotalUnifiedLocks is the total number of unified locks.
	TotalUnifiedLocks int

	// TotalFiles is the number of files with any locks.
	TotalFiles int

	// BreakCallbackCount is the number of registered break callbacks.
	BreakCallbackCount int

	// GracePeriodActive indicates if grace period is active.
	GracePeriodActive bool
}

ManagerStats contains statistics about the lock manager state.

type NLMHolderInfo

type NLMHolderInfo struct {
	// CallerName identifies the lock holder.
	// For SMB leases: "smb:<clientID>"
	// For NLM locks: Original caller_name from lock request
	CallerName string

	// Svid is the server-unique identifier (process ID in Unix terms).
	// For SMB leases: Always 0 (SMB has no process ID concept)
	// For NLM locks: Original svid from lock request
	Svid int32

	// OH is the owner handle (opaque identifier).
	// For SMB leases: First 8 bytes of the 128-bit LeaseKey
	// For NLM locks: Original oh from lock request
	OH []byte

	// Offset is the starting byte offset of the lock.
	// For SMB leases: Always 0 (whole file)
	// For NLM locks: Original offset from lock
	Offset uint64

	// Length is the number of bytes locked.
	// For SMB leases: ^uint64(0) (max value = whole file)
	// For NLM locks: Original length from lock
	Length uint64

	// Exclusive indicates if this is an exclusive (write) lock.
	// For SMB leases: true if lease has Write caching permission
	// For NLM locks: true if exclusive lock type
	Exclusive bool
}

NLMHolderInfo represents lock holder information in NLM-compatible format.

This struct is used to construct NLM4_DENIED responses when an NFS client attempts to acquire a lock that conflicts with an SMB lease. The NLM protocol expects specific fields to identify the lock holder.

Cross-Protocol Semantics (per CONTEXT.md):

  • CallerName: "smb:<clientID>" identifies the SMB client
  • Svid: 0 (SMB has no concept of process ID like Unix)
  • OH: First 8 bytes of LeaseKey (owner handle)
  • Offset: 0 (leases are whole-file)
  • Length: ^uint64(0) (max value, meaning whole file)
  • Exclusive: true if lease has Write caching permission

func TranslateByteRangeLockToNLMHolder

func TranslateByteRangeLockToNLMHolder(lock *UnifiedLock) NLMHolderInfo

TranslateByteRangeLockToNLMHolder converts a byte-range lock to NLM holder format.

This is used when an NFS client's lock request conflicts with another byte-range lock (from NLM or SMB). The translation preserves the original lock's range information.

Parameters:

  • lock: The UnifiedLock representing a byte-range lock (Lease == nil)

Returns:

  • NLMHolderInfo: NLM-compatible holder information

func TranslateToNLMHolder

func TranslateToNLMHolder(lease *UnifiedLock) NLMHolderInfo

TranslateToNLMHolder converts an SMB lease to NLM holder format.

This is used to construct NLM4_DENIED responses when an NFS client's lock request conflicts with an SMB lease. The translation follows the semantics defined in CONTEXT.md.

Parameters:

  • lease: The UnifiedLock representing an SMB lease (must have Lease != nil)

Returns:

  • NLMHolderInfo: NLM-compatible holder information

Panics:

  • If lease is nil or lease.Lease is nil (not a valid lease)

Example:

lease := getConflictingLease(...)
holderInfo := TranslateToNLMHolder(lease)
// Use holderInfo fields in NLM4_DENIED response

type NSMCallback

type NSMCallback struct {
	// Hostname is the callback target (my_id.my_name).
	// This is where the SM_NOTIFY RPC will be sent.
	Hostname string

	// Program is the RPC program number (usually NLM 100021).
	Program uint32

	// Version is the program version.
	Version uint32

	// Proc is the procedure number for the callback.
	// NLM uses NLM_FREE_ALL (procedure 23) to release locks.
	Proc uint32
}

NSMCallback holds callback RPC details from SM_MON my_id field. Used to send SM_NOTIFY callbacks when server restarts or client crashes.

type OpLock

type OpLock struct {
	// LeaseKey is the 128-bit client-generated key identifying this lease.
	// Multiple file handles with the same key share the lease.
	LeaseKey [16]byte

	// LeaseState is the current lease state (R/W/H flags bitwise OR'd).
	// Use HasRead(), HasWrite(), HasHandle() to check individual flags.
	LeaseState uint32

	// BreakToState is the target state during an active break.
	// Zero if no break is in progress.
	BreakToState uint32

	// Breaking indicates a lease break is in progress awaiting acknowledgment.
	// When true, the client has been notified and must acknowledge.
	Breaking bool

	// Epoch is incremented on every lease state change (SMB3).
	// Used by clients to detect stale state notifications.
	Epoch uint16

	// BreakStarted records when the break was initiated.
	// Used to enforce break timeout (force revoke if client doesn't acknowledge).
	BreakStarted time.Time

	// Reclaim indicates this lease was reclaimed during grace period.
	// Set when SMB client reconnects after server restart and successfully
	// reclaims its previously held lease.
	Reclaim bool
}

OpLock holds SMB2/3 lease-specific state.

A lease provides caching permissions (R/W/H) to SMB clients. Unlike byte-range locks, leases are whole-file and identified by a client-generated 128-bit key. Multiple file handles with the same LeaseKey share the lease state.

Lease Break Flow:

  1. Conflicting operation detected (e.g., NFS write to file with SMB Write lease)
  2. Server sets Breaking=true, BreakToState=target state
  3. Server sends LEASE_BREAK_NOTIFICATION to client
  4. Client flushes dirty data (if Write lease), acknowledges break
  5. Server updates LeaseState to BreakToState, clears Breaking

Reference: MS-SMB2 3.3.5.9.11 Processing a Lease-Break Acknowledgment

func (*OpLock) Clone

func (l *OpLock) Clone() *OpLock

Clone creates a deep copy of the OpLock.

func (*OpLock) HasHandle

func (l *OpLock) HasHandle() bool

HasHandle returns true if the lease includes Handle caching permission.

func (*OpLock) HasRead

func (l *OpLock) HasRead() bool

HasRead returns true if the lease includes Read caching permission.

func (*OpLock) HasWrite

func (l *OpLock) HasWrite() bool

HasWrite returns true if the lease includes Write caching permission.

func (*OpLock) IsBreaking

func (l *OpLock) IsBreaking() bool

IsBreaking returns true if a lease break is in progress.

func (*OpLock) StateString

func (l *OpLock) StateString() string

StateString returns a human-readable string representation of the lease state. Examples: "None", "R", "RW", "RH", "RWH"

type OpLockBreakCallback

type OpLockBreakCallback interface {
	// OnLeaseBreakTimeout is called when a lease break times out without acknowledgment.
	// The lease has already been force-revoked (deleted from store).
	OnLeaseBreakTimeout(leaseKey [16]byte)
}

OpLockBreakCallback is called when a lease break times out. The callback allows the OplockManager to clean up internal state.

type OpLockBreakScanner

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

OpLockBreakScanner monitors breaking leases and force-revokes on timeout.

The scanner runs in the background, periodically checking for leases that are in the "breaking" state and have exceeded the timeout.

When a break times out:

  1. The lease is deleted from the store (force-revoked)
  2. The callback is notified so it can clean up tracking state
  3. The conflicting operation can proceed

func NewOpLockBreakScanner

func NewOpLockBreakScanner(
	lockStore LockStore,
	callback OpLockBreakCallback,
	timeout time.Duration,
) *OpLockBreakScanner

NewOpLockBreakScanner creates a new lease break scanner.

Parameters:

  • lockStore: The lock store to query for breaking leases
  • callback: Called when a break times out (can be nil)
  • timeout: Break timeout (0 = DefaultOpLockBreakTimeout)

func NewOpLockBreakScannerWithInterval

func NewOpLockBreakScannerWithInterval(
	lockStore LockStore,
	callback OpLockBreakCallback,
	timeout time.Duration,
	scanInterval time.Duration,
) *OpLockBreakScanner

NewOpLockBreakScannerWithInterval creates a new lease break scanner with custom scan interval. This is primarily useful for testing.

func (*OpLockBreakScanner) GetTimeout

func (s *OpLockBreakScanner) GetTimeout() time.Duration

GetTimeout returns the current break timeout.

func (*OpLockBreakScanner) IsRunning

func (s *OpLockBreakScanner) IsRunning() bool

IsRunning returns true if the scanner is currently running.

func (*OpLockBreakScanner) SetTimeout

func (s *OpLockBreakScanner) SetTimeout(timeout time.Duration)

SetTimeout updates the break timeout. This only affects future timeout calculations, not breaks already in progress.

func (*OpLockBreakScanner) Start

func (s *OpLockBreakScanner) Start()

Start begins the background scan loop. Safe to call multiple times (subsequent calls are no-ops).

func (*OpLockBreakScanner) Stop

func (s *OpLockBreakScanner) Stop()

Stop stops the background scan loop. Blocks until the loop has exited. Safe to call multiple times.

type Operation

type Operation struct {
	// IsReclaim indicates this is a lock reclaim during grace period.
	IsReclaim bool

	// IsTest indicates this is a lock test (query) operation.
	IsTest bool

	// IsNew indicates this is a new lock request (not reclaim or test).
	IsNew bool
}

Operation describes the type of lock operation for grace period checking.

type PersistedClientRegistration

type PersistedClientRegistration struct {
	// ClientID is the unique identifier (e.g., hostname or IP).
	ClientID string

	// MonName is the monitored hostname (from SM_MON mon_id.mon_name).
	// This identifies what host the client is monitoring.
	MonName string

	// Priv is the 16-byte private data for callbacks.
	// Returned unchanged in SM_NOTIFY to help client identify recovery context.
	Priv [16]byte

	// CallbackHost is the callback target hostname (from my_id.my_name).
	// This is where SM_NOTIFY RPCs will be sent.
	CallbackHost string

	// CallbackProg is the RPC program number for callbacks.
	// Typically NLM program number (100021).
	CallbackProg uint32

	// CallbackVers is the program version for callbacks.
	CallbackVers uint32

	// CallbackProc is the procedure number for callbacks.
	// NLM uses NLM_FREE_ALL (procedure 23) to release locks.
	CallbackProc uint32

	// RegisteredAt is when the registration was created.
	RegisteredAt time.Time

	// ServerEpoch is the server epoch at registration time.
	// Used to detect stale registrations from previous server instances.
	ServerEpoch uint64
}

PersistedClientRegistration is the storage representation of a client registration. Used to persist NSM client registrations across server restarts. This enables the server to send SM_NOTIFY callbacks to previously registered clients when the server restarts.

func ToPersistedClientRegistration

func ToPersistedClientRegistration(reg *ClientRegistration, serverEpoch uint64) *PersistedClientRegistration

ToPersistedClientRegistration converts a ClientRegistration to its persisted form. This is used when saving registrations to the store.

type PersistedLock

type PersistedLock struct {
	// ID is the unique identifier for this lock (UUID).
	ID string `json:"id"`

	// ShareName is the share this lock belongs to.
	ShareName string `json:"share_name"`

	// FileID is the file identifier (string representation of FileHandle).
	FileID string `json:"file_id"`

	// OwnerID is the protocol-provided owner identifier.
	// Format: "{protocol}:{details}" - treated as opaque.
	OwnerID string `json:"owner_id"`

	// ClientID is the connection tracker client ID.
	// Used to clean up locks when a client disconnects.
	ClientID string `json:"client_id"`

	// LockType indicates shared (0) or exclusive (1).
	LockType int `json:"lock_type"`

	// Offset is the starting byte offset of the lock.
	Offset uint64 `json:"offset"`

	// Length is the number of bytes locked (0 = to EOF).
	Length uint64 `json:"length"`

	// AccessMode is the SMB share mode (0=none, 1=deny-read, 2=deny-write, 3=deny-all).
	AccessMode int `json:"share_reservation"`

	// AcquiredAt is when the lock was acquired.
	AcquiredAt time.Time `json:"acquired_at"`

	// ServerEpoch is the server epoch when the lock was acquired.
	// Used for split-brain detection and stale lock cleanup.
	ServerEpoch uint64 `json:"server_epoch"`

	// LeaseKey is the 128-bit client-generated key identifying the lease.
	// Non-empty (16 bytes) for leases, empty for byte-range locks.
	LeaseKey []byte `json:"lease_key,omitempty"`

	// LeaseState is the current R/W/H flags (LeaseStateRead|Write|Handle).
	// 0 for byte-range locks or None lease state.
	LeaseState uint32 `json:"lease_state,omitempty"`

	// LeaseEpoch is the SMB3 epoch counter, incremented on state change.
	// 0 for byte-range locks.
	LeaseEpoch uint16 `json:"lease_epoch,omitempty"`

	// BreakToState is the target state during an active lease break.
	// 0 if no break in progress.
	BreakToState uint32 `json:"break_to_state,omitempty"`

	// Breaking indicates a lease break is in progress awaiting acknowledgment.
	// False for byte-range locks.
	Breaking bool `json:"breaking,omitempty"`
}

PersistedLock represents a lock stored in the metadata store.

This is the serializable form of UnifiedLock, designed for persistence across server restarts. All protocol-specific information is encoded in the OwnerID field as an opaque string.

Persistence enables:

  • Lock recovery after server restart
  • Grace period for clients to reclaim locks
  • Split-brain detection via ServerEpoch
  • SMB lease state persistence and reclaim

func ToPersistedLock

func ToPersistedLock(lock *UnifiedLock, epoch uint64) *PersistedLock

ToPersistedLock converts an UnifiedLock to a PersistedLock for storage.

Parameters:

  • lock: The in-memory lock to persist
  • epoch: Current server epoch to stamp on the lock

Returns:

  • *PersistedLock: Serializable lock ready for storage

For leases, the Lease field must be non-nil. The 128-bit LeaseKey, LeaseState, Epoch, BreakToState, and Breaking are all preserved.

func (*PersistedLock) IsLease

func (pl *PersistedLock) IsLease() bool

IsLease returns true if this persisted lock is an SMB lease.

type Stats

type Stats struct {
	// TotalLocks is the total number of active locks
	TotalLocks int

	// UniqueFiles is the number of files with at least one lock
	UniqueFiles int

	// UniqueClients is the number of clients with at least one lock
	UniqueClients int

	// MaxLocksOnFile is the highest lock count on any single file
	MaxLocksOnFile int

	// MaxLocksForClient is the highest lock count for any single client
	MaxLocksForClient int
}

Stats contains current lock usage statistics.

type UnifiedLock

type UnifiedLock struct {
	// ID is a unique identifier for this lock (UUID).
	// Used for lock management, debugging, and metrics.
	ID string

	// Owner identifies who holds the lock.
	Owner LockOwner

	// FileHandle is the file this lock is on.
	// This is the store-specific file handle.
	FileHandle FileHandle

	// Offset is the starting byte offset of the lock.
	// For leases, this is always 0 (whole-file).
	Offset uint64

	// Length is the number of bytes locked.
	// 0 means "to end of file" (unbounded).
	// For leases, this is always 0 (whole-file).
	Length uint64

	// Type indicates whether this is a shared or exclusive lock.
	// For leases, this reflects the lease type:
	//   - LockTypeShared for Read-only leases
	//   - LockTypeExclusive for Write-containing leases
	Type LockType

	// AccessMode is the SMB share mode (NFS protocols ignore this).
	AccessMode AccessMode

	// AcquiredAt is when the lock was acquired.
	AcquiredAt time.Time

	// Blocking indicates whether this was a blocking (wait) request.
	// Non-blocking requests fail immediately on conflict.
	Blocking bool

	// Reclaim indicates whether this is a reclaim during grace period.
	// Reclaim locks have priority over new locks during grace period.
	Reclaim bool

	// Lease holds lease-specific state for SMB2/3 leases.
	// Nil for byte-range locks; non-nil for leases.
	// When non-nil, Offset=0 and Length=0 (whole-file).
	Lease *OpLock
}

UnifiedLock represents a byte-range lock or SMB lease with full protocol support.

This extends the basic FileLock concept to support:

  • Protocol-agnostic ownership (NLM, SMB, NFSv4)
  • SMB share reservations
  • SMB2/3 leases (R/W/H caching via Lease field)
  • Reclaim tracking for grace periods
  • Lock identification for management

Lock Lifecycle:

  1. Client requests lock via protocol handler
  2. Lock manager checks for conflicts using OwnerID comparison
  3. If no conflict, lock is acquired with unique ID
  4. Lock persists until: explicitly released, file closed, session ends, or server restarts

Lease vs Byte-Range Lock:

  • Byte-range locks: Offset/Length define locked range, Lease is nil
  • Leases: Whole-file (Offset=0, Length=0), Lease contains R/W/H state
  • Use IsLease() to distinguish between the two

Cross-Protocol Behavior: All protocols share the same lock namespace. An NLM lock on bytes 0-100 will conflict with an SMB lock request for the same range, enabling unified locking across protocols. Leases also participate in cross-protocol conflict detection (e.g., NFS write triggers SMB Write lease break).

func FromPersistedLock

func FromPersistedLock(pl *PersistedLock) *UnifiedLock

FromPersistedLock converts a PersistedLock back to an UnifiedLock.

Parameters:

  • pl: The persisted lock from storage

Returns:

  • *UnifiedLock: In-memory lock for use in lock manager

For leases (identified by non-empty LeaseKey), the OpLock struct is populated with the persisted lease state. Blocking and Reclaim are runtime-only and not restored.

func MergeLocks

func MergeLocks(locks []*UnifiedLock) []*UnifiedLock

MergeLocks coalesces adjacent or overlapping locks from the same owner.

This is used when upgrading or extending locks to avoid fragmentation. Only locks with the same owner, type, and file handle can be merged.

Parameters:

  • locks: Slice of locks to potentially merge

Returns:

  • []UnifiedLock: Merged locks (may have fewer elements than input)

func NewUnifiedLock

func NewUnifiedLock(owner LockOwner, fileHandle FileHandle, offset, length uint64, lockType LockType) *UnifiedLock

NewUnifiedLock creates a new UnifiedLock with a generated UUID.

func SplitLock

func SplitLock(existing *UnifiedLock, unlockOffset, unlockLength uint64) []*UnifiedLock

SplitLock splits an existing lock when a portion is unlocked.

POSIX semantics require that unlocking a portion of a locked range results in:

  • 0 locks: if the unlock range covers the entire lock
  • 1 lock: if the unlock range covers the start or end
  • 2 locks: if the unlock range is in the middle (creates a "hole")

Parameters:

  • existing: The lock to split
  • unlockOffset: Starting byte offset of the unlock range
  • unlockLength: Number of bytes to unlock (0 = to EOF)

Returns:

  • []UnifiedLock: The resulting locks after the split (0, 1, or 2 locks)

Examples:

  • Lock [0-100], Unlock [0-100] -> [] (exact match)
  • Lock [0-100], Unlock [0-50] -> [[50-100]] (unlock at start)
  • Lock [0-100], Unlock [50-100] -> [[0-50]] (unlock at end)
  • Lock [0-100], Unlock [25-75] -> [[0-25], [75-100]] (unlock in middle)

func (*UnifiedLock) Clone

func (ul *UnifiedLock) Clone() *UnifiedLock

Clone creates a deep copy of the lock.

func (*UnifiedLock) ConflictsWith

func (ul *UnifiedLock) ConflictsWith(other *UnifiedLock) bool

ConflictsWith checks if this lock conflicts with another lock.

This method handles all 4 conflict cases:

  1. Access mode conflicts (SMB deny modes)
  2. OpLock vs OpLock (lease-to-lease conflicts)
  3. OpLock vs byte-range (cross-type conflicts)
  4. Byte-range vs byte-range (traditional range overlap + type check)

Same owner never conflicts (allows re-locking and upgrading).

Returns true if the locks conflict and one must be denied or broken.

func (*UnifiedLock) Contains

func (ul *UnifiedLock) Contains(offset, length uint64) bool

Contains returns true if this lock fully contains the specified range.

func (*UnifiedLock) End

func (ul *UnifiedLock) End() uint64

End returns the end offset of the lock (exclusive). Returns 0 for unbounded locks (Length=0 means to EOF).

func (*UnifiedLock) IsExclusive

func (ul *UnifiedLock) IsExclusive() bool

IsExclusive returns true if this is an exclusive (write) lock.

func (*UnifiedLock) IsLease

func (ul *UnifiedLock) IsLease() bool

IsLease returns true if this is an SMB2/3 lease rather than a byte-range lock. Leases have the Lease field set and are whole-file (Offset=0, Length=0).

func (*UnifiedLock) IsShared

func (ul *UnifiedLock) IsShared() bool

IsShared returns true if this is a shared (read) lock.

func (*UnifiedLock) Overlaps

func (ul *UnifiedLock) Overlaps(offset, length uint64) bool

Overlaps returns true if this lock overlaps with the specified range.

type UnifiedLockConflict

type UnifiedLockConflict struct {
	// Lock is the conflicting lock.
	Lock *UnifiedLock

	// Reason describes why the conflict occurred.
	Reason string
}

UnifiedLockConflict describes a conflicting lock for error reporting.

type WaitForGraph

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

WaitForGraph implements deadlock detection using a Wait-For Graph (WFG).

In a WFG, nodes represent lock owners (by OwnerID) and directed edges represent "is waiting for" relationships. A cycle in this graph indicates a deadlock condition.

Example deadlock:

  • Owner A holds lock L1, waits for lock L2 (held by B)
  • Owner B holds lock L2, waits for lock L1 (held by A)
  • Graph: A -> B -> A (cycle = deadlock)

Usage:

  1. Before blocking on a lock, call WouldCauseCycle() to check for deadlock
  2. If no cycle, call AddWaiter() to record the wait relationship
  3. When lock is granted or request times out, call RemoveWaiter()
  4. When lock is released, call RemoveOwner() to clear all wait relationships

Thread Safety: WaitForGraph is safe for concurrent use by multiple goroutines.

func NewWaitForGraph

func NewWaitForGraph() *WaitForGraph

NewWaitForGraph creates a new empty Wait-For Graph.

func (*WaitForGraph) AddWaiter

func (wfg *WaitForGraph) AddWaiter(waiter string, owners []string)

AddWaiter records that waiter is waiting for all specified owners.

IMPORTANT: Caller MUST call WouldCauseCycle first to ensure this won't create a deadlock. This method does NOT check for cycles.

Parameters:

  • waiter: The owner ID that is now waiting
  • owners: The owner IDs being waited on

func (*WaitForGraph) GetWaitersFor

func (wfg *WaitForGraph) GetWaitersFor(owner string) []string

GetWaitersFor returns all owners that are waiting for the specified owner.

This is useful when a lock is released - we can notify these waiters that they may be able to proceed.

Parameters:

  • owner: The owner ID to find waiters for

Returns:

  • Slice of owner IDs that are waiting for this owner

func (*WaitForGraph) RemoveOwner

func (wfg *WaitForGraph) RemoveOwner(owner string)

RemoveOwner removes all wait relationships where owner is the target.

Call this when:

  • Owner releases a lock
  • Owner's session disconnects

This also removes the owner as a waiter (if they were waiting for something).

Parameters:

  • owner: The owner ID to remove from the graph

func (*WaitForGraph) RemoveWaiter

func (wfg *WaitForGraph) RemoveWaiter(waiter string)

RemoveWaiter removes all wait relationships where waiter is the source.

Call this when:

  • Lock is granted to the waiter
  • Lock request is cancelled
  • Lock request times out

Parameters:

  • waiter: The owner ID that is no longer waiting

func (*WaitForGraph) Size

func (wfg *WaitForGraph) Size() int

Size returns the number of owners currently waiting (for testing/monitoring).

func (*WaitForGraph) WouldCauseCycle

func (wfg *WaitForGraph) WouldCauseCycle(waiter string, owners []string) bool

WouldCauseCycle checks if adding edges from waiter to owners would create a cycle.

This MUST be called before blocking on a lock request. If it returns true, the lock request should be denied with ErrDeadlock instead of blocking.

Parameters:

  • waiter: The owner ID that wants to wait
  • owners: The owner IDs currently holding the conflicting lock(s)

Returns:

  • true if adding these wait relationships would create a cycle (deadlock)
  • false if it's safe to proceed with waiting

Algorithm: For each owner in owners, perform DFS from that owner to see if we can reach the waiter. If any path exists, adding waiter -> owner would create a cycle.

Jump to

Keyboard shortcuts

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