Files
seaweedfs/weed/filer/posixlock/codec_test.go
T
Chris Lu 475ae2b443 filer: serialize the POSIX lock set for entry metadata (phase 2) (#9661)
* filer: POSIX advisory lock set primitive (phase 1)

Pure per-inode conflict/coalesce/range-split logic for fcntl byte-range
and flock whole-file locks, extracted from the mount's PosixLockTable
without its wait queue or inode-map concurrency. Owner identity is
(Sid, Owner) so the same FUSE owner on different mounts never aliases,
and ReleaseSession reaps a dead mount's locks. The owner filer will hold
one Set per inode under the per-path lock; no concurrency control here.

* test: tolerate transient FUSE invisibility in ConcurrentReadWrite

A concurrent truncating overwrite leaves a short-lived dentry/cache window
where the file is momentarily ENOENT to another opener. Retry the reads and
writes a few times before failing, as ConcurrentDirectoryOperations does.

* filer: serialize the POSIX lock set for entry metadata

Versioned fixed-width binary encoding of a Set, so an inode's held locks
can ride in its entry metadata: a lock op materializes the Set from the
blob, applies under the per-path lock, and writes it back. Empty set
encodes to nil so a lock-free inode carries no blob.

* filer: encode the POSIX lock set as protobuf

Replace the hand-rolled fixed-width codec with a LockSetProto message, so the
metadata blob can gain fields without a format-version migration. proto.Unmarshal
already rejects a malformed blob, so the explicit version and length checks go
away. Marshal now returns an error to match.
2026-05-24 22:33:36 -07:00

76 lines
2.2 KiB
Go

package posixlock
import (
"math"
"reflect"
"testing"
)
func TestMarshalEmptyIsNil(t *testing.T) {
s := &Set{}
b, err := s.Marshal()
if err != nil {
t.Fatalf("marshal empty: %v", err)
}
if b != nil {
t.Fatalf("empty set should marshal to nil, got %d bytes", len(b))
}
got, err := Unmarshal(nil)
if err != nil {
t.Fatalf("unmarshal nil: %v", err)
}
if !got.Empty() {
t.Fatal("unmarshal nil should be an empty set")
}
}
func TestMarshalRoundTrip(t *testing.T) {
s := &Set{}
// A spread of fields: EOF range, both namespaces, two sessions, distinct pids.
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Sid: 1, Owner: 7, Pid: 10})
mustAcquire(t, s, Range{Start: 200, End: math.MaxUint64, Type: Read, Sid: 2, Owner: 7, Pid: 20})
mustAcquire(t, s, Range{Start: 0, End: math.MaxUint64, Type: Write, Sid: 2, Owner: 9, Pid: 30, IsFlock: true})
b, err := s.Marshal()
if err != nil {
t.Fatalf("marshal: %v", err)
}
got, err := Unmarshal(b)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if !reflect.DeepEqual(got.Locks(), s.Locks()) {
t.Fatalf("round-trip mismatch:\n got %+v\nwant %+v", got.Locks(), s.Locks())
}
}
func TestUnmarshalRejectsMalformed(t *testing.T) {
// field 1 (locks), wire type 2 (length-delimited) declaring 5 bytes that
// are not there: a corrupt blob must fail loudly, not read back as empty.
if _, err := Unmarshal([]byte{0x0A, 0x05}); err == nil {
t.Fatal("expected an error on malformed input")
}
}
// A decoded set must behave identically — conflict detection still fires.
func TestRoundTripPreservesConflict(t *testing.T) {
s := &Set{}
mustAcquire(t, s, Range{Start: 0, End: 99, Type: Write, Sid: 1, Owner: 1, Pid: 10})
b, err := s.Marshal()
if err != nil {
t.Fatalf("marshal: %v", err)
}
decoded, err := Unmarshal(b)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if _, granted := decoded.Acquire(Range{Start: 50, End: 149, Type: Write, Sid: 2, Owner: 1, Pid: 20}); granted {
t.Fatal("decoded set should still report the conflict")
}
// Same owner across the wire is still the same owner.
if _, granted := decoded.Acquire(Range{Start: 0, End: 99, Type: Read, Sid: 1, Owner: 1, Pid: 10}); !granted {
t.Fatal("decoded set should still recognize the same (sid,owner)")
}
}