snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
This commit is contained in:
259
weed/filer/posixlock/manager.go
Normal file
259
weed/filer/posixlock/manager.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package posixlock
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Manager is the owner filer's in-memory authority for POSIX advisory locks
|
||||
// across inodes. Lock state lives here, not in replicated metadata: it is
|
||||
// transient coordination, so keeping it out of the meta-log avoids churn and
|
||||
// does not pollute what subscribers see (the distributed lock manager holds its
|
||||
// locks the same way). A `Set` per inode key, plus a session index so a dead
|
||||
// mount's locks are reaped in O(locks held) rather than by scanning every inode.
|
||||
//
|
||||
// key is an opaque inode identity supplied by the caller — the file's path, or
|
||||
// "hl:"+hex(HardLinkId) for a hardlinked inode — so all names of one inode share
|
||||
// a Set. The Manager is safe for concurrent use.
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
byKey map[string]*Set // inode key -> held locks
|
||||
bySid map[uint64]map[string]bool // session -> keys it currently holds locks on
|
||||
lastSeen map[uint64]time.Time // session -> last keepalive; only renewing sessions are leased
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
byKey: make(map[string]*Set),
|
||||
bySid: make(map[uint64]map[string]bool),
|
||||
lastSeen: make(map[uint64]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
// Renew records a keepalive from a session, placing it under lease management.
|
||||
// Only sessions that have renewed are subject to ReapExpired, so a session that
|
||||
// never sends keepalives (e.g. before the mount keepalive exists) is never reaped.
|
||||
func (m *Manager) Renew(sid uint64) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.lastSeen[sid] = time.Now()
|
||||
}
|
||||
|
||||
// ReapExpired releases the locks of every leased session whose last keepalive is
|
||||
// older than ttl — a dead or partitioned mount. Sessions that never renewed are
|
||||
// left untouched. Returns the reaped session ids.
|
||||
func (m *Manager) ReapExpired(ttl time.Duration) []uint64 {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
cutoff := time.Now().Add(-ttl)
|
||||
var reaped []uint64
|
||||
for sid, seen := range m.lastSeen {
|
||||
if seen.After(cutoff) {
|
||||
continue
|
||||
}
|
||||
for key := range m.bySid[sid] {
|
||||
s := m.byKey[key]
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
s.ReleaseSession(sid)
|
||||
if s.Empty() {
|
||||
delete(m.byKey, key)
|
||||
}
|
||||
}
|
||||
delete(m.bySid, sid)
|
||||
delete(m.lastSeen, sid)
|
||||
reaped = append(reaped, sid)
|
||||
}
|
||||
return reaped
|
||||
}
|
||||
|
||||
// TryLock grants lk on key, or returns the conflicting lock and false. The set
|
||||
// is created on first use and dropped again when it empties.
|
||||
func (m *Manager) TryLock(key string, lk Range) (Range, bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
s, ok := m.byKey[key]
|
||||
if !ok {
|
||||
s = &Set{}
|
||||
}
|
||||
if c, granted := s.Acquire(lk); !granted {
|
||||
return c, false
|
||||
}
|
||||
if !ok {
|
||||
m.byKey[key] = s
|
||||
}
|
||||
m.index(lk.Sid, key)
|
||||
return Range{}, true
|
||||
}
|
||||
|
||||
// Track records a lock the server already granted, without arbitration. A mount
|
||||
// uses it to mirror its own held locks so it can re-assert them to the inode's
|
||||
// current owner filer after an ownership change or owner restart.
|
||||
func (m *Manager) Track(key string, lk Range) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
s, ok := m.byKey[key]
|
||||
if !ok {
|
||||
s = &Set{}
|
||||
m.byKey[key] = s
|
||||
}
|
||||
s.Grant(lk)
|
||||
m.index(lk.Sid, key)
|
||||
}
|
||||
|
||||
// Snapshot returns a copy of the held locks per key. A mount calls it to drive
|
||||
// re-assertion keepalives; the filer never does.
|
||||
func (m *Manager) Snapshot() map[string][]Range {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
out := make(map[string][]Range, len(m.byKey))
|
||||
for key, s := range m.byKey {
|
||||
out[key] = append([]Range(nil), s.locks...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Reassert rebuilds session sid's locks on key from the client's authoritative
|
||||
// list, renewing the lease. It replaces sid's existing locks on the key, then
|
||||
// re-acquires each asserted lock — arbitrating against other sessions so it never
|
||||
// double-grants. Locks that lost to another session in a migration window are
|
||||
// returned as conflicts. The owner filer calls this on a re-assertion keepalive.
|
||||
func (m *Manager) Reassert(key string, sid uint64, locks []Range) (conflicts []Range) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.lastSeen[sid] = time.Now()
|
||||
|
||||
s := m.byKey[key]
|
||||
if s == nil {
|
||||
if len(locks) == 0 {
|
||||
return nil
|
||||
}
|
||||
s = &Set{}
|
||||
m.byKey[key] = s
|
||||
}
|
||||
s.ReleaseSession(sid)
|
||||
for _, lk := range locks {
|
||||
lk.Sid = sid
|
||||
if c, granted := s.Acquire(lk); !granted {
|
||||
conflicts = append(conflicts, c)
|
||||
}
|
||||
}
|
||||
if setHasSession(s, sid) {
|
||||
m.index(sid, key)
|
||||
} else {
|
||||
m.deindex(sid, key)
|
||||
}
|
||||
if s.Empty() {
|
||||
delete(m.byKey, key)
|
||||
}
|
||||
return conflicts
|
||||
}
|
||||
|
||||
// Unlock releases lk's owner's locks within its namespace over lk's range.
|
||||
func (m *Manager) Unlock(key string, lk Range) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
s := m.byKey[key]
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.Release(lk)
|
||||
m.afterRelease(key, s, lk.Sid)
|
||||
}
|
||||
|
||||
// GetLk reports the lock that would block proposed on key, if any.
|
||||
func (m *Manager) GetLk(key string, proposed Range) (Range, bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
s := m.byKey[key]
|
||||
if s == nil {
|
||||
return Range{}, false
|
||||
}
|
||||
return s.Conflict(proposed)
|
||||
}
|
||||
|
||||
// ReleasePosixOwner drops (sid, owner)'s fcntl locks on key — the flush-time path.
|
||||
func (m *Manager) ReleasePosixOwner(key string, sid, owner uint64) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
s := m.byKey[key]
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.ReleasePosixOwner(sid, owner)
|
||||
m.afterRelease(key, s, sid)
|
||||
}
|
||||
|
||||
// ReleaseFlockOwner drops (sid, owner)'s flock locks on key — the release-time path.
|
||||
func (m *Manager) ReleaseFlockOwner(key string, sid, owner uint64) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
s := m.byKey[key]
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.ReleaseFlockOwner(sid, owner)
|
||||
m.afterRelease(key, s, sid)
|
||||
}
|
||||
|
||||
// ReleaseSession drops every lock held by a session across all inodes it touched,
|
||||
// reaping a mount whose lease expired. O(locks held by the session).
|
||||
func (m *Manager) ReleaseSession(sid uint64) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for key := range m.bySid[sid] {
|
||||
s := m.byKey[key]
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
s.ReleaseSession(sid)
|
||||
if s.Empty() {
|
||||
delete(m.byKey, key)
|
||||
}
|
||||
}
|
||||
delete(m.bySid, sid)
|
||||
}
|
||||
|
||||
// afterRelease prunes the session index when sid no longer holds any lock on key,
|
||||
// and drops the set when it empties. Only sid's presence can have changed, since
|
||||
// a release only removes sid's locks.
|
||||
func (m *Manager) afterRelease(key string, s *Set, sid uint64) {
|
||||
if s.Empty() {
|
||||
delete(m.byKey, key)
|
||||
m.deindex(sid, key)
|
||||
return
|
||||
}
|
||||
if !setHasSession(s, sid) {
|
||||
m.deindex(sid, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) index(sid uint64, key string) {
|
||||
keys := m.bySid[sid]
|
||||
if keys == nil {
|
||||
keys = make(map[string]bool)
|
||||
m.bySid[sid] = keys
|
||||
}
|
||||
keys[key] = true
|
||||
}
|
||||
|
||||
func (m *Manager) deindex(sid uint64, key string) {
|
||||
keys := m.bySid[sid]
|
||||
if keys == nil {
|
||||
return
|
||||
}
|
||||
delete(keys, key)
|
||||
if len(keys) == 0 {
|
||||
delete(m.bySid, sid)
|
||||
}
|
||||
}
|
||||
|
||||
func setHasSession(s *Set, sid uint64) bool {
|
||||
for _, l := range s.Locks() {
|
||||
if l.Sid == sid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
194
weed/filer/posixlock/manager_test.go
Normal file
194
weed/filer/posixlock/manager_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package posixlock
|
||||
|
||||
import (
|
||||
"math"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestManagerGrantAndConflict(t *testing.T) {
|
||||
m := NewManager()
|
||||
if _, granted := m.TryLock("a", Range{Start: 0, End: 99, Type: Write, Sid: 1, Owner: 1}); !granted {
|
||||
t.Fatal("first lock should be granted")
|
||||
}
|
||||
if c, granted := m.TryLock("a", Range{Start: 50, End: 149, Type: Write, Sid: 2, Owner: 1}); granted {
|
||||
t.Fatalf("overlapping lock from another session should conflict, got grant; conflict=%+v", c)
|
||||
}
|
||||
// A different key is independent.
|
||||
if _, granted := m.TryLock("b", Range{Start: 0, End: 99, Type: Write, Sid: 2, Owner: 1}); !granted {
|
||||
t.Fatal("lock on a different key should be granted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerUnlockCleansEmptyKeyAndIndex(t *testing.T) {
|
||||
m := NewManager()
|
||||
lk := Range{Start: 0, End: 99, Type: Write, Sid: 1, Owner: 1}
|
||||
m.TryLock("a", lk)
|
||||
|
||||
if !m.bySid[1]["a"] {
|
||||
t.Fatal("session index should record the held key")
|
||||
}
|
||||
m.Unlock("a", Range{Start: 0, End: 99, Type: Unlock, Sid: 1, Owner: 1})
|
||||
|
||||
if _, ok := m.byKey["a"]; ok {
|
||||
t.Fatal("empty set should be dropped from byKey")
|
||||
}
|
||||
if _, ok := m.bySid[1]; ok {
|
||||
t.Fatal("session index should be pruned when it holds nothing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerPartialUnlockKeepsIndex(t *testing.T) {
|
||||
m := NewManager()
|
||||
m.TryLock("a", Range{Start: 0, End: 49, Type: Write, Sid: 1, Owner: 1})
|
||||
m.TryLock("a", Range{Start: 100, End: 149, Type: Write, Sid: 1, Owner: 1})
|
||||
// Release one of the two ranges; the session still holds the other.
|
||||
m.Unlock("a", Range{Start: 0, End: 49, Type: Unlock, Sid: 1, Owner: 1})
|
||||
|
||||
if !m.bySid[1]["a"] {
|
||||
t.Fatal("session still holds a lock on the key; index must remain")
|
||||
}
|
||||
if _, ok := m.byKey["a"]; !ok {
|
||||
t.Fatal("key should remain while a lock is held")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerGetLk(t *testing.T) {
|
||||
m := NewManager()
|
||||
m.TryLock("a", Range{Start: 10, End: 50, Type: Write, Sid: 1, Owner: 1, Pid: 7})
|
||||
c, found := m.GetLk("a", Range{Start: 30, End: 70, Type: Read, Sid: 2, Owner: 1})
|
||||
if !found || c.Pid != 7 {
|
||||
t.Fatalf("expected conflict from pid 7, got %+v found=%v", c, found)
|
||||
}
|
||||
if _, found := m.GetLk("missing", Range{Start: 0, End: 1, Type: Write, Sid: 9, Owner: 9}); found {
|
||||
t.Fatal("missing key should report no conflict")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerReleasePosixOwnerKeepsFlockAndIndex(t *testing.T) {
|
||||
m := NewManager()
|
||||
m.TryLock("a", Range{Start: 0, End: 99, Type: Write, Sid: 1, Owner: 1})
|
||||
m.TryLock("a", Range{Start: 0, End: math.MaxUint64, Type: Write, Sid: 1, Owner: 1, IsFlock: true})
|
||||
|
||||
m.ReleasePosixOwner("a", 1, 1)
|
||||
|
||||
// flock lock for the same session remains, so the index must remain too.
|
||||
if !m.bySid[1]["a"] {
|
||||
t.Fatal("session still holds the flock lock; index must remain")
|
||||
}
|
||||
if _, found := m.GetLk("a", Range{Start: 0, End: 10, Type: Write, Sid: 2, Owner: 2, IsFlock: true}); !found {
|
||||
t.Fatal("flock lock should survive ReleasePosixOwner")
|
||||
}
|
||||
if _, found := m.GetLk("a", Range{Start: 0, End: 10, Type: Write, Sid: 2, Owner: 2}); found {
|
||||
t.Fatal("fcntl lock should be gone after ReleasePosixOwner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerReleaseSessionReapsAcrossKeys(t *testing.T) {
|
||||
m := NewManager()
|
||||
m.TryLock("a", Range{Start: 0, End: 99, Type: Write, Sid: 1, Owner: 1})
|
||||
m.TryLock("b", Range{Start: 0, End: 99, Type: Write, Sid: 1, Owner: 2})
|
||||
m.TryLock("b", Range{Start: 200, End: 299, Type: Write, Sid: 2, Owner: 1})
|
||||
|
||||
m.ReleaseSession(1)
|
||||
|
||||
if _, ok := m.bySid[1]; ok {
|
||||
t.Fatal("reaped session should be gone from the index")
|
||||
}
|
||||
if _, ok := m.byKey["a"]; ok {
|
||||
t.Fatal("key a held only session 1's lock and should be dropped")
|
||||
}
|
||||
// Session 2's lock on b survives.
|
||||
if _, found := m.GetLk("b", Range{Start: 200, End: 299, Type: Write, Sid: 9, Owner: 9}); !found {
|
||||
t.Fatal("session 2's lock on b should remain after reaping session 1")
|
||||
}
|
||||
if !m.bySid[2]["b"] {
|
||||
t.Fatal("session 2 index entry should remain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerReapsOnlyStaleLeasedSessions(t *testing.T) {
|
||||
m := NewManager()
|
||||
// Session 1: holds a lock, leased but stale (renewed long ago).
|
||||
m.TryLock("a", Range{Start: 0, End: 99, Type: Write, Sid: 1, Owner: 1})
|
||||
m.Renew(1)
|
||||
m.lastSeen[1] = time.Now().Add(-time.Hour)
|
||||
// Session 2: holds a lock, leased and fresh.
|
||||
m.TryLock("b", Range{Start: 0, End: 99, Type: Write, Sid: 2, Owner: 1})
|
||||
m.Renew(2)
|
||||
// Session 3: holds a lock but never renewed (no lease) — must not be reaped.
|
||||
m.TryLock("c", Range{Start: 0, End: 99, Type: Write, Sid: 3, Owner: 1})
|
||||
|
||||
reaped := m.ReapExpired(30 * time.Second)
|
||||
|
||||
if len(reaped) != 1 || reaped[0] != 1 {
|
||||
t.Fatalf("only the stale leased session should be reaped, got %v", reaped)
|
||||
}
|
||||
if _, ok := m.byKey["a"]; ok {
|
||||
t.Fatal("stale session's lock should be gone")
|
||||
}
|
||||
if _, ok := m.byKey["b"]; !ok {
|
||||
t.Fatal("fresh session's lock must remain")
|
||||
}
|
||||
if _, ok := m.byKey["c"]; !ok {
|
||||
t.Fatal("never-renewed session must not be reaped")
|
||||
}
|
||||
if _, ok := m.lastSeen[1]; ok {
|
||||
t.Fatal("reaped session's lease entry should be cleared")
|
||||
}
|
||||
}
|
||||
|
||||
// Mutual exclusion under concurrent whole-file flock churn through the Manager:
|
||||
// at most one owner may believe it holds the exclusive lock at any instant.
|
||||
func TestManagerConcurrentFlockMutualExclusion(t *testing.T) {
|
||||
m := NewManager()
|
||||
const (
|
||||
key = "inode"
|
||||
workers = 16
|
||||
iters = 400
|
||||
)
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
holder atomic.Int64
|
||||
overlap atomic.Int32
|
||||
)
|
||||
for w := 0; w < workers; w++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
lk := Range{Start: 0, End: math.MaxUint64, Type: Write, Sid: uint64(id + 1), Owner: 1, IsFlock: true}
|
||||
unlock := lk
|
||||
unlock.Type = Unlock
|
||||
token := int64(id + 1)
|
||||
for i := 0; i < iters; i++ {
|
||||
for {
|
||||
if _, granted := m.TryLock(key, lk); granted {
|
||||
break
|
||||
}
|
||||
runtime.Gosched()
|
||||
}
|
||||
if prev := holder.Swap(token); prev != 0 {
|
||||
overlap.Add(1)
|
||||
}
|
||||
runtime.Gosched()
|
||||
if !holder.CompareAndSwap(token, 0) {
|
||||
overlap.Add(1)
|
||||
}
|
||||
m.Unlock(key, unlock)
|
||||
}
|
||||
}(w)
|
||||
}
|
||||
wg.Wait()
|
||||
if n := overlap.Load(); n != 0 {
|
||||
t.Fatalf("mutual exclusion violated %d times", n)
|
||||
}
|
||||
if len(m.byKey) != 0 {
|
||||
t.Fatalf("all locks released; byKey should be empty, got %d", len(m.byKey))
|
||||
}
|
||||
if len(m.bySid) != 0 {
|
||||
t.Fatalf("all locks released; bySid should be empty, got %d", len(m.bySid))
|
||||
}
|
||||
}
|
||||
222
weed/filer/posixlock/posixlock.go
Normal file
222
weed/filer/posixlock/posixlock.go
Normal file
@@ -0,0 +1,222 @@
|
||||
// Package posixlock implements the conflict, coalescing, and range-split logic
|
||||
// for POSIX advisory file locks — fcntl byte-range and flock whole-file — as a
|
||||
// pure per-inode lock set with no concurrency control of its own.
|
||||
//
|
||||
// It is the server-side authority for distributed FUSE locking: the owner filer
|
||||
// for an inode holds one Set and serializes access to it under that inode's
|
||||
// per-path lock, so each operation runs to completion without concurrent
|
||||
// mutation. The same algorithm can back the per-mount table; blocking (SetLkw)
|
||||
// and any wait queue belong to the caller, not here.
|
||||
package posixlock
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Lock types, kept independent of the platform syscall package (whose F_RDLCK /
|
||||
// F_WRLCK / F_UNLCK values differ per OS and are absent on some) so the filer
|
||||
// builds everywhere. Callers map the syscall constants onto these at the edge.
|
||||
// Zero is intentionally unused so a zero-value Range reads as "unset".
|
||||
const (
|
||||
Read uint32 = 1
|
||||
Write uint32 = 2
|
||||
Unlock uint32 = 3
|
||||
)
|
||||
|
||||
// Range is one held advisory byte-range lock. Owner identity is the (Sid, Owner)
|
||||
// pair: Sid is the mount session, Owner the FUSE lock owner within it, so owners
|
||||
// from different mounts never alias. End is inclusive; math.MaxUint64 means EOF.
|
||||
// IsFlock separates the flock and fcntl namespaces, which never conflict.
|
||||
type Range struct {
|
||||
Start uint64
|
||||
End uint64
|
||||
Type uint32
|
||||
Sid uint64
|
||||
Owner uint64
|
||||
Pid uint32
|
||||
IsFlock bool
|
||||
}
|
||||
|
||||
func (r Range) sameOwner(o Range) bool {
|
||||
return r.Sid == o.Sid && r.Owner == o.Owner
|
||||
}
|
||||
|
||||
// Set is the authoritative set of advisory locks held on one inode. The zero
|
||||
// value is an empty set. Set has no internal locking; the caller serializes it.
|
||||
type Set struct {
|
||||
locks []Range // sorted by Start
|
||||
}
|
||||
|
||||
func overlap(aStart, aEnd, bStart, bEnd uint64) bool {
|
||||
return aStart <= bEnd && bStart <= aEnd
|
||||
}
|
||||
|
||||
// Conflict returns the first held lock that blocks proposed, if any. Two locks
|
||||
// conflict when they share a namespace, have different owners, overlap, and at
|
||||
// least one is a write lock.
|
||||
func (s *Set) Conflict(proposed Range) (Range, bool) {
|
||||
for _, h := range s.locks {
|
||||
if h.IsFlock != proposed.IsFlock || h.sameOwner(proposed) {
|
||||
continue
|
||||
}
|
||||
if !overlap(h.Start, h.End, proposed.Start, proposed.End) {
|
||||
continue
|
||||
}
|
||||
if h.Type == Read && proposed.Type == Read {
|
||||
continue
|
||||
}
|
||||
return h, true
|
||||
}
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
// Acquire grants lk when it does not conflict, inserting it and coalescing the
|
||||
// owner's adjacent/overlapping ranges. On conflict it returns the blocking lock
|
||||
// and false, leaving the set unchanged.
|
||||
func (s *Set) Acquire(lk Range) (Range, bool) {
|
||||
if c, found := s.Conflict(lk); found {
|
||||
return c, false
|
||||
}
|
||||
s.insert(lk)
|
||||
return Range{}, true
|
||||
}
|
||||
|
||||
// Grant inserts lk without a conflict check. It is for a client mirroring locks
|
||||
// the server already granted (so they are conflict-free), not for arbitration.
|
||||
func (s *Set) Grant(lk Range) {
|
||||
s.insert(lk)
|
||||
}
|
||||
|
||||
// insert adds lk, absorbing same-owner same-type overlaps and merging adjacent
|
||||
// same-type ranges, and truncating/splitting a same-owner range of a different
|
||||
// type that overlaps (an in-place type change).
|
||||
func (s *Set) insert(lk Range) {
|
||||
var kept []Range
|
||||
for _, h := range s.locks {
|
||||
if !h.sameOwner(lk) || h.IsFlock != lk.IsFlock {
|
||||
kept = append(kept, h)
|
||||
continue
|
||||
}
|
||||
if !overlap(h.Start, h.End, lk.Start, lk.End) {
|
||||
// Merge only ranges that are adjacent and the same type. The
|
||||
// End < MaxUint64 guards stop +1 from wrapping at EOF.
|
||||
if h.Type == lk.Type && ((h.End < math.MaxUint64 && h.End+1 == lk.Start) || (lk.End < math.MaxUint64 && lk.End+1 == h.Start)) {
|
||||
if h.Start < lk.Start {
|
||||
lk.Start = h.Start
|
||||
}
|
||||
if h.End > lk.End {
|
||||
lk.End = h.End
|
||||
}
|
||||
continue
|
||||
}
|
||||
kept = append(kept, h)
|
||||
continue
|
||||
}
|
||||
if h.Type == lk.Type {
|
||||
// Same type: absorb into lk by widening its range.
|
||||
if h.Start < lk.Start {
|
||||
lk.Start = h.Start
|
||||
}
|
||||
if h.End > lk.End {
|
||||
lk.End = h.End
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Different type: the surviving portions of h outside lk's range stay.
|
||||
if h.Start < lk.Start {
|
||||
left := h
|
||||
left.End = lk.Start - 1
|
||||
kept = append(kept, left)
|
||||
}
|
||||
if h.End > lk.End {
|
||||
right := h
|
||||
right.Start = lk.End + 1
|
||||
kept = append(kept, right)
|
||||
}
|
||||
}
|
||||
kept = append(kept, lk)
|
||||
sort.Slice(kept, func(i, j int) bool { return kept[i].Start < kept[j].Start })
|
||||
s.locks = kept
|
||||
}
|
||||
|
||||
// remove drops or splits matching locks within [start,end]. A match that
|
||||
// straddles the range keeps its non-overlapping head and/or tail.
|
||||
func (s *Set) remove(matches func(Range) bool, start, end uint64) {
|
||||
var kept []Range
|
||||
for _, h := range s.locks {
|
||||
if !matches(h) || !overlap(h.Start, h.End, start, end) {
|
||||
kept = append(kept, h)
|
||||
continue
|
||||
}
|
||||
if h.Start < start {
|
||||
left := h
|
||||
left.End = start - 1
|
||||
kept = append(kept, left)
|
||||
}
|
||||
if h.End > end {
|
||||
right := h
|
||||
right.Start = end + 1
|
||||
kept = append(kept, right)
|
||||
}
|
||||
// Fully covered: dropped.
|
||||
}
|
||||
s.locks = kept
|
||||
}
|
||||
|
||||
// Release clears lk's owner's locks within lk's namespace over [lk.Start,lk.End]
|
||||
// — the F_UNLCK path, which may split a straddling range.
|
||||
func (s *Set) Release(lk Range) {
|
||||
s.remove(func(h Range) bool {
|
||||
return h.sameOwner(lk) && h.IsFlock == lk.IsFlock
|
||||
}, lk.Start, lk.End)
|
||||
}
|
||||
|
||||
// ReleaseOwner removes every lock held by (sid, owner) in both namespaces.
|
||||
func (s *Set) ReleaseOwner(sid, owner uint64) {
|
||||
s.remove(func(h Range) bool {
|
||||
return h.Sid == sid && h.Owner == owner
|
||||
}, 0, math.MaxUint64)
|
||||
}
|
||||
|
||||
// ReleaseFlockOwner removes only the flock locks of (sid, owner) — the close-time
|
||||
// path for a released file description (FUSE_RELEASE_FLOCK_UNLOCK).
|
||||
func (s *Set) ReleaseFlockOwner(sid, owner uint64) {
|
||||
s.remove(func(h Range) bool {
|
||||
return h.IsFlock && h.Sid == sid && h.Owner == owner
|
||||
}, 0, math.MaxUint64)
|
||||
}
|
||||
|
||||
// ReleasePosixOwner removes only the fcntl locks of (sid, owner) — the close-time
|
||||
// path for a flushing POSIX lock owner.
|
||||
func (s *Set) ReleasePosixOwner(sid, owner uint64) {
|
||||
s.remove(func(h Range) bool {
|
||||
return !h.IsFlock && h.Sid == sid && h.Owner == owner
|
||||
}, 0, math.MaxUint64)
|
||||
}
|
||||
|
||||
// ReleaseSession removes every lock held by a session, reaping a mount that has
|
||||
// died or disconnected (its lease expired).
|
||||
func (s *Set) ReleaseSession(sid uint64) {
|
||||
s.remove(func(h Range) bool { return h.Sid == sid }, 0, math.MaxUint64)
|
||||
}
|
||||
|
||||
// HasPosix reports whether (sid, owner) holds any fcntl lock, mirroring the
|
||||
// mount's flush-time check that avoids treating a lock-free flush as
|
||||
// lock-sensitive.
|
||||
func (s *Set) HasPosix(sid, owner uint64) bool {
|
||||
for _, h := range s.locks {
|
||||
if !h.IsFlock && h.Sid == sid && h.Owner == owner {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Locks returns the held locks, sorted by Start. The slice aliases internal
|
||||
// state; the caller must not mutate it.
|
||||
func (s *Set) Locks() []Range { return s.locks }
|
||||
|
||||
// Empty reports whether no locks are held, so the caller can drop the inode's
|
||||
// entry from its table.
|
||||
func (s *Set) Empty() bool { return len(s.locks) == 0 }
|
||||
292
weed/filer/posixlock/posixlock_test.go
Normal file
292
weed/filer/posixlock/posixlock_test.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package posixlock
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// acquire is a test helper asserting the lock is granted.
|
||||
func mustAcquire(t *testing.T, s *Set, lk Range) {
|
||||
t.Helper()
|
||||
if _, granted := s.Acquire(lk); !granted {
|
||||
t.Fatalf("expected lock granted: %+v", lk)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonOverlappingLocksFromDifferentOwners(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 49, Type: Write, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 50, End: 99, Type: Write, Owner: 2, Pid: 20})
|
||||
}
|
||||
|
||||
func TestOverlappingReadLocksFromDifferentOwners(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Read, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 50, End: 149, Type: Read, Owner: 2, Pid: 20})
|
||||
}
|
||||
|
||||
func TestOverlappingWriteReadConflict(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Owner: 1, Pid: 10})
|
||||
if _, granted := s.Acquire(Range{Start: 50, End: 149, Type: Read, Owner: 2, Pid: 20}); granted {
|
||||
t.Fatal("expected conflict")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlappingWriteWriteConflict(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Owner: 1, Pid: 10})
|
||||
if _, granted := s.Acquire(Range{Start: 50, End: 149, Type: Write, Owner: 2, Pid: 20}); granted {
|
||||
t.Fatal("expected conflict")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameOwnerUpgradeReadToWrite(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Read, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Owner: 1, Pid: 10})
|
||||
|
||||
c, found := s.Conflict(Range{Start: 0, End: 99, Type: Write, Owner: 2, Pid: 20})
|
||||
if !found || c.Type != Write {
|
||||
t.Fatalf("expected conflicting write lock after upgrade, got %+v found=%v", c, found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameOwnerDowngradeWriteToRead(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Read, Owner: 1, Pid: 10})
|
||||
// Another owner can now take a shared read lock.
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Read, Owner: 2, Pid: 20})
|
||||
}
|
||||
|
||||
func TestLockCoalescing(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 9, Type: Write, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 10, End: 19, Type: Write, Owner: 1, Pid: 10})
|
||||
|
||||
if len(s.locks) != 1 {
|
||||
t.Fatalf("expected 1 coalesced lock, got %d: %+v", len(s.locks), s.locks)
|
||||
}
|
||||
if s.locks[0].Start != 0 || s.locks[0].End != 19 {
|
||||
t.Errorf("expected coalesced [0,19], got [%d,%d]", s.locks[0].Start, s.locks[0].End)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockSplitting(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Owner: 1, Pid: 10})
|
||||
s.Release(Range{Start: 40, End: 59, Type: Unlock, Owner: 1, Pid: 10})
|
||||
|
||||
if len(s.locks) != 2 {
|
||||
t.Fatalf("expected 2 locks after split, got %d: %+v", len(s.locks), s.locks)
|
||||
}
|
||||
if s.locks[0].Start != 0 || s.locks[0].End != 39 {
|
||||
t.Errorf("expected left [0,39], got [%d,%d]", s.locks[0].Start, s.locks[0].End)
|
||||
}
|
||||
if s.locks[1].Start != 60 || s.locks[1].End != 99 {
|
||||
t.Errorf("expected right [60,99], got [%d,%d]", s.locks[1].Start, s.locks[1].End)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictReportsHolder(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 10, End: 50, Type: Write, Owner: 1, Pid: 10})
|
||||
|
||||
c, found := s.Conflict(Range{Start: 30, End: 70, Type: Read, Owner: 2, Pid: 20})
|
||||
if !found {
|
||||
t.Fatal("expected a conflict")
|
||||
}
|
||||
if c.Type != Write || c.Pid != 10 || c.Start != 10 || c.End != 50 {
|
||||
t.Fatalf("unexpected conflict report: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictNoneForSharedReads(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 10, End: 50, Type: Read, Owner: 1, Pid: 10})
|
||||
if _, found := s.Conflict(Range{Start: 30, End: 70, Type: Read, Owner: 2, Pid: 20}); found {
|
||||
t.Fatal("two read locks should not conflict")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictSameOwnerNone(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Owner: 1, Pid: 10})
|
||||
if _, found := s.Conflict(Range{Start: 0, End: 99, Type: Write, Owner: 1, Pid: 10}); found {
|
||||
t.Fatal("an owner should not conflict with itself")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseOwner(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 49, Type: Write, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 50, End: 99, Type: Write, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 200, End: 299, Type: Read, Owner: 2, Pid: 20})
|
||||
|
||||
s.ReleaseOwner(0, 1)
|
||||
|
||||
if _, found := s.Conflict(Range{Start: 0, End: 99, Type: Write, Owner: 3, Pid: 30}); found {
|
||||
t.Fatal("owner 1's locks should be gone")
|
||||
}
|
||||
c, found := s.Conflict(Range{Start: 200, End: 299, Type: Write, Owner: 3, Pid: 30})
|
||||
if !found || c.Type != Read {
|
||||
t.Fatalf("owner 2's read lock should remain, got %+v found=%v", c, found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlockAndFcntlDoNotConflict(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 0, End: math.MaxUint64, Type: Write, Owner: 2, Pid: 20, IsFlock: true})
|
||||
}
|
||||
|
||||
func TestReleasePosixOwnerKeepsFlock(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: math.MaxUint64, Type: Write, Owner: 1, Pid: 10, IsFlock: true})
|
||||
s.ReleasePosixOwner(0, 1)
|
||||
if _, found := s.Conflict(Range{Start: 0, End: math.MaxUint64, Type: Write, Owner: 2, Pid: 20, IsFlock: true}); !found {
|
||||
t.Fatal("flock lock should remain after ReleasePosixOwner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseFlockOwnerKeepsPosix(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 0, End: math.MaxUint64, Type: Write, Owner: 2, Pid: 10, IsFlock: true})
|
||||
|
||||
s.ReleaseFlockOwner(0, 2)
|
||||
|
||||
if _, found := s.Conflict(Range{Start: 0, End: 99, Type: Write, Owner: 3, Pid: 30}); !found {
|
||||
t.Fatal("fcntl lock should remain after ReleaseFlockOwner")
|
||||
}
|
||||
if _, found := s.Conflict(Range{Start: 0, End: math.MaxUint64, Type: Write, Owner: 4, Pid: 40, IsFlock: true}); found {
|
||||
t.Fatal("flock lock should be gone after ReleaseFlockOwner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasPosixIgnoresMissingOwnerAndFlock(t *testing.T) {
|
||||
s := &Set{}
|
||||
if s.HasPosix(0, 1) {
|
||||
t.Fatal("empty set should report no posix owner")
|
||||
}
|
||||
mustAcquire(t, s, Range{Start: 0, End: math.MaxUint64, Type: Write, Owner: 1, Pid: 10, IsFlock: true})
|
||||
if s.HasPosix(0, 1) {
|
||||
t.Fatal("a flock owner is not a posix owner")
|
||||
}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Owner: 2, Pid: 20})
|
||||
if !s.HasPosix(0, 2) {
|
||||
t.Fatal("posix owner should be reported")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWholeFileLock(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: math.MaxUint64, Type: Write, Owner: 1, Pid: 10})
|
||||
if _, granted := s.Acquire(Range{Start: 0, End: math.MaxUint64, Type: Write, Owner: 2, Pid: 20}); granted {
|
||||
t.Fatal("whole-file lock should block another owner")
|
||||
}
|
||||
if _, granted := s.Acquire(Range{Start: 100, End: 200, Type: Read, Owner: 2, Pid: 20}); granted {
|
||||
t.Fatal("partial overlap with whole-file lock should conflict")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseNoExistingLocks(t *testing.T) {
|
||||
s := &Set{}
|
||||
s.Release(Range{Start: 0, End: 99, Type: Unlock, Owner: 1, Pid: 10})
|
||||
if !s.Empty() {
|
||||
t.Fatal("releasing on an empty set should be a no-op")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameOwnerReplaceDifferentType(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 30, End: 60, Type: Read, Owner: 1, Pid: 10})
|
||||
|
||||
if len(s.locks) != 3 {
|
||||
t.Fatalf("expected 3 locks after partial type change, got %d: %+v", len(s.locks), s.locks)
|
||||
}
|
||||
if s.locks[0].Type != Write || s.locks[0].Start != 0 || s.locks[0].End != 29 {
|
||||
t.Errorf("expected write [0,29], got %+v", s.locks[0])
|
||||
}
|
||||
if s.locks[1].Type != Read || s.locks[1].Start != 30 || s.locks[1].End != 60 {
|
||||
t.Errorf("expected read [30,60], got %+v", s.locks[1])
|
||||
}
|
||||
if s.locks[2].Type != Write || s.locks[2].Start != 61 || s.locks[2].End != 99 {
|
||||
t.Errorf("expected write [61,99], got %+v", s.locks[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonAdjacentRangesNotCoalesced(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 5, End: math.MaxUint64, Type: Write, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 0, End: 2, Type: Write, Owner: 1, Pid: 10})
|
||||
|
||||
if len(s.locks) != 2 {
|
||||
t.Fatalf("gap [3,4] should prevent coalescing, got %d: %+v", len(s.locks), s.locks)
|
||||
}
|
||||
if s.locks[0].Start != 0 || s.locks[0].End != 2 {
|
||||
t.Errorf("expected [0,2], got [%d,%d]", s.locks[0].Start, s.locks[0].End)
|
||||
}
|
||||
if s.locks[1].Start != 5 || s.locks[1].End != math.MaxUint64 {
|
||||
t.Errorf("expected [5,MaxUint64], got [%d,%d]", s.locks[1].Start, s.locks[1].End)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdjacencyNoOverflowAtMaxUint64(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 100, End: math.MaxUint64, Type: Write, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 0, End: 0, Type: Write, Owner: 1, Pid: 10})
|
||||
|
||||
if len(s.locks) != 2 {
|
||||
t.Fatalf("MaxUint64+1 must not wrap and falsely merge, got %d: %+v", len(s.locks), s.locks)
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions are part of owner identity: the same FUSE Owner number on two
|
||||
// different mounts (Sid) is two distinct owners and must contend.
|
||||
func TestTwoSessionsSameOwnerDoNotAlias(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Sid: 1, Owner: 5, Pid: 10})
|
||||
|
||||
if _, granted := s.Acquire(Range{Start: 50, End: 149, Type: Write, Sid: 2, Owner: 5, Pid: 20}); granted {
|
||||
t.Fatal("same Owner number on a different session must conflict, not alias")
|
||||
}
|
||||
// A read on session 2 against a session-1 read is fine (shared).
|
||||
s2 := &Set{}
|
||||
mustAcquire(t, s2, Range{Start: 0, End: 99, Type: Read, Sid: 1, Owner: 5})
|
||||
mustAcquire(t, s2, Range{Start: 0, End: 99, Type: Read, Sid: 2, Owner: 5})
|
||||
}
|
||||
|
||||
// Reaping a dead mount drops only that session's locks.
|
||||
func TestReleaseSessionReapsOnlyThatSession(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 49, Type: Write, Sid: 1, Owner: 1, Pid: 10})
|
||||
mustAcquire(t, s, Range{Start: 0, End: math.MaxUint64, Type: Write, Sid: 1, Owner: 2, Pid: 11, IsFlock: true})
|
||||
mustAcquire(t, s, Range{Start: 50, End: 99, Type: Write, Sid: 2, Owner: 1, Pid: 20})
|
||||
|
||||
s.ReleaseSession(1)
|
||||
|
||||
for _, h := range s.locks {
|
||||
if h.Sid == 1 {
|
||||
t.Fatalf("session 1 lock survived reaping: %+v", h)
|
||||
}
|
||||
}
|
||||
c, found := s.Conflict(Range{Start: 50, End: 99, Type: Write, Sid: 3, Owner: 9})
|
||||
if !found || c.Sid != 2 {
|
||||
t.Fatalf("session 2's lock should remain, got %+v found=%v", c, found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyAfterReleasingAll(t *testing.T) {
|
||||
s := &Set{}
|
||||
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Owner: 1, Pid: 10})
|
||||
if s.Empty() {
|
||||
t.Fatal("set should not be empty with a held lock")
|
||||
}
|
||||
s.Release(Range{Start: 0, End: 99, Type: Unlock, Owner: 1, Pid: 10})
|
||||
if !s.Empty() {
|
||||
t.Fatal("set should be empty after releasing the only lock")
|
||||
}
|
||||
}
|
||||
82
weed/filer/posixlock/reassert_test.go
Normal file
82
weed/filer/posixlock/reassert_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package posixlock
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A mount's tracked locks round-trip through Snapshot back to a fresh owner via
|
||||
// Reassert — the owner-restart / ring-change recovery path.
|
||||
func TestReassertRebuildsOnFreshOwner(t *testing.T) {
|
||||
const sid = uint64(7)
|
||||
|
||||
// Client mirror: two granted locks on one key, one on another.
|
||||
client := NewManager()
|
||||
client.Track("a", Range{Start: 0, End: 99, Type: Write, Sid: sid, Owner: 1})
|
||||
client.Track("a", Range{Start: 200, End: 299, Type: Read, Sid: sid, Owner: 2})
|
||||
client.Track("b", Range{Start: 0, End: maxEnd, Type: Write, Sid: sid, Owner: 1, IsFlock: true})
|
||||
|
||||
// Fresh owner (post-restart / new ring owner) knows nothing.
|
||||
owner := NewManager()
|
||||
for key, locks := range client.Snapshot() {
|
||||
if c := owner.Reassert(key, sid, locks); c != nil {
|
||||
t.Fatalf("unexpected conflict reasserting %s: %+v", key, c)
|
||||
}
|
||||
}
|
||||
|
||||
// The owner now reports the same conflicts a foreign session would hit.
|
||||
if _, granted := owner.TryLock("a", Range{Start: 50, End: 60, Type: Write, Sid: 99, Owner: 1}); granted {
|
||||
t.Fatal("owner should block a foreign write after rebuild")
|
||||
}
|
||||
if _, granted := owner.TryLock("b", Range{Start: 0, End: 0, Type: Read, Sid: 99, Owner: 1, IsFlock: true}); granted {
|
||||
t.Fatal("owner should block a foreign flock read after rebuild")
|
||||
}
|
||||
}
|
||||
|
||||
// Re-asserting every tick is idempotent: the owner's view is unchanged.
|
||||
func TestReassertIdempotent(t *testing.T) {
|
||||
const sid = uint64(1)
|
||||
m := NewManager()
|
||||
m.TryLock("k", Range{Start: 0, End: 99, Type: Write, Sid: sid, Owner: 1})
|
||||
before := append([]Range(nil), m.byKey["k"].locks...)
|
||||
|
||||
m.Reassert("k", sid, before)
|
||||
m.Reassert("k", sid, before)
|
||||
|
||||
if !reflect.DeepEqual(m.byKey["k"].locks, before) {
|
||||
t.Fatalf("reassert not idempotent:\n got %+v\nwant %+v", m.byKey["k"].locks, before)
|
||||
}
|
||||
}
|
||||
|
||||
// A lock another session grabbed in the migration window is reported as a
|
||||
// conflict and not double-granted.
|
||||
func TestReassertReportsConflict(t *testing.T) {
|
||||
const mine, other = uint64(1), uint64(2)
|
||||
m := NewManager()
|
||||
// Another mount took the lock on this (new) owner during the gap.
|
||||
m.TryLock("k", Range{Start: 0, End: 99, Type: Write, Sid: other, Owner: 1})
|
||||
|
||||
conflicts := m.Reassert("k", mine, []Range{{Start: 0, End: 99, Type: Write, Sid: mine, Owner: 1}})
|
||||
if len(conflicts) != 1 {
|
||||
t.Fatalf("expected 1 conflict, got %d: %+v", len(conflicts), conflicts)
|
||||
}
|
||||
// The other session keeps the lock; mine was not installed.
|
||||
if got := len(m.byKey["k"].locks); got != 1 {
|
||||
t.Fatalf("expected only the incumbent lock, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Reassert renews the lease, so a re-asserting mount is not reaped.
|
||||
func TestReassertRenewsLease(t *testing.T) {
|
||||
const sid = uint64(1)
|
||||
m := NewManager()
|
||||
m.Renew(sid)
|
||||
m.Reassert("k", sid, []Range{{Start: 0, End: 9, Type: Write, Sid: sid, Owner: 1}})
|
||||
|
||||
if reaped := m.ReapExpired(time.Hour); len(reaped) != 0 {
|
||||
t.Fatalf("freshly re-asserted session should not be reaped: %v", reaped)
|
||||
}
|
||||
}
|
||||
|
||||
const maxEnd = ^uint64(0)
|
||||
Reference in New Issue
Block a user