mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 12:16:36 +00:00
* log buffer: wake the interval loop on shutdown instead of sleeping through it loopInterval parked in time.Sleep(flushInterval) and only re-checked IsStopping when it woke, so a buffer shut down early kept both loop goroutines - and the PreviousBufferCount+1 slabs of BufferSize they reach - alive for up to a full interval afterwards. Select on shutdownCh against a ticker instead, and give the loops a WaitGroup so a test can observe that they exit. * test: release the filers the server tests build Every helper here left its filer's meta log buffer running, so each test pinned PreviousBufferCount+1 buffers of BufferSize for the rest of the run: ~3.5GB of live heap across the package, which overruns the address space on linux/386 and kills the 32-bit job with an out-of-memory throw. Thread the test through the helpers so the buffer is shut down on cleanup, and shut the subscribe harness's filer down outright - its deletion loop keeps the whole filer reachable otherwise. That harness quiesces its flush path first, since Filer.Shutdown closes the store a flush still in flight would write through.
188 lines
6.9 KiB
Go
188 lines
6.9 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func chunkFid(fid string) *filer_pb.FileChunk {
|
|
return &filer_pb.FileChunk{FileId: fid, Size: 100}
|
|
}
|
|
|
|
func ifChunksEqual(fids ...string) *filer_pb.WriteCondition {
|
|
return one(&filer_pb.WriteCondition_Clause{Kind: filer_pb.WriteCondition_IF_CHUNKS_EQUAL, Fids: fids})
|
|
}
|
|
|
|
func TestChunkFidsEqualClause(t *testing.T) {
|
|
withChunks := func(fids ...string) *filer.Entry {
|
|
e := &filer.Entry{FullPath: "/test/obj"}
|
|
for _, fid := range fids {
|
|
e.Chunks = append(e.Chunks, chunkFid(fid))
|
|
}
|
|
return e
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
cur *filer.Entry
|
|
fids []string
|
|
want bool
|
|
}{
|
|
{"absent-no-chunks", nil, nil, true},
|
|
{"absent-expected", nil, []string{"3,01"}, false},
|
|
{"empty-expects-none", withChunks(), nil, true},
|
|
{"exact", withChunks("3,01", "3,02"), []string{"3,01", "3,02"}, true},
|
|
{"reordered", withChunks("3,01", "3,02"), []string{"3,02", "3,01"}, true},
|
|
{"duplicates", withChunks("3,01", "3,01", "3,02"), []string{"3,02", "3,01", "3,01"}, true},
|
|
// The strand: a concurrent update emptied the stored chunk list, so the
|
|
// stale writer's expectation no longer holds.
|
|
{"stored-emptied", withChunks(), []string{"3,01"}, false},
|
|
// The reverse strand: the writer read no chunks, but a concurrent update
|
|
// has since added one; an empty fids list still guards it.
|
|
{"stored-filled", withChunks("3,01"), nil, false},
|
|
{"chunk-added", withChunks("3,01", "3,02"), []string{"3,01"}, false},
|
|
{"chunk-removed", withChunks("3,01"), []string{"3,01", "3,02"}, false},
|
|
{"chunk-replaced", withChunks("3,09"), []string{"3,01"}, false},
|
|
{"duplicate-count", withChunks("3,01"), []string{"3,01", "3,01"}, false},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := writeConditionSatisfied(ifChunksEqual(tc.fids...), tc.cur); got != tc.want {
|
|
t.Errorf("%s: got %v want %v", tc.name, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUpdateEntryChunkConditionPreventsStrand drives the UpdateEntry handler
|
|
// through the interleaving that strands an entry on a dead needle:
|
|
//
|
|
// 1. entry has chunks=[F]; needle F is live.
|
|
// 2. R (a chunk-preserving read-modify-write) snapshots chunks=[F].
|
|
// 3. D (eviction) commits chunks=[] first; F is diffed away and queued for
|
|
// deletion. The store is seeded at chunks=[] to model D's commit.
|
|
// 4. R commits its stale snapshot last.
|
|
//
|
|
// Without a condition R's write wins and resurrects the reference to F; with
|
|
// IF_CHUNKS_EQUAL it fails with FailedPrecondition and the entry stays empty.
|
|
func TestUpdateEntryChunkConditionPreventsStrand(t *testing.T) {
|
|
const fidF = "3,01637037d6"
|
|
|
|
newServer := func(storedChunks ...*filer_pb.FileChunk) (*FilerServer, *renameTestStore) {
|
|
store := newRenameTestStore()
|
|
store.entries["/test/obj"] = &filer.Entry{
|
|
FullPath: "/test/obj",
|
|
Attr: filer.Attr{Inode: 1, Mtime: time.Unix(1700000000, 0), Mode: 0644},
|
|
Chunks: storedChunks,
|
|
}
|
|
f := newRenameTestFiler(t, store)
|
|
return &FilerServer{filer: f, option: &FilerOption{}, entryLockTable: util.NewLockTable[util.FullPath]()}, store
|
|
}
|
|
|
|
// R's stale request: it read chunks=[F] and writes back chunks=[F].
|
|
staleReq := func(cond *filer_pb.WriteCondition) *filer_pb.UpdateEntryRequest {
|
|
return &filer_pb.UpdateEntryRequest{
|
|
Directory: "/test",
|
|
Entry: &filer_pb.Entry{
|
|
Name: "obj",
|
|
Attributes: &filer_pb.FuseAttributes{Mtime: 1700000005, FileMode: 0644, Inode: 1},
|
|
Chunks: []*filer_pb.FileChunk{chunkFid(fidF)},
|
|
},
|
|
Condition: cond,
|
|
}
|
|
}
|
|
|
|
t.Run("unconditional-resurrects", func(t *testing.T) {
|
|
fs, store := newServer()
|
|
if _, err := fs.UpdateEntry(context.Background(), staleReq(nil)); err != nil {
|
|
t.Fatalf("UpdateEntry: %v", err)
|
|
}
|
|
if got := len(store.entries["/test/obj"].GetChunks()); got != 1 {
|
|
t.Fatalf("last-write-wins should have resurrected chunks=[F], got %d chunks", got)
|
|
}
|
|
})
|
|
|
|
t.Run("condition-rejects-stale", func(t *testing.T) {
|
|
fs, store := newServer()
|
|
_, err := fs.UpdateEntry(context.Background(), staleReq(ifChunksEqual(fidF)))
|
|
if status.Code(err) != codes.FailedPrecondition {
|
|
t.Fatalf("stale write must fail with FailedPrecondition, got %v", err)
|
|
}
|
|
if got := len(store.entries["/test/obj"].GetChunks()); got != 0 {
|
|
t.Fatalf("entry must stay chunks=[], got %d chunks", got)
|
|
}
|
|
})
|
|
|
|
t.Run("condition-passes-fresh", func(t *testing.T) {
|
|
fs, store := newServer(chunkFid(fidF))
|
|
if _, err := fs.UpdateEntry(context.Background(), staleReq(ifChunksEqual(fidF))); err != nil {
|
|
t.Fatalf("fresh read-modify-write must succeed: %v", err)
|
|
}
|
|
chunks := store.entries["/test/obj"].GetChunks()
|
|
if len(chunks) != 1 || chunks[0].GetFileIdString() != fidF {
|
|
t.Fatalf("chunks=[F] must be preserved, got %v", chunks)
|
|
}
|
|
})
|
|
|
|
// The reverse strand: R read the entry before a concurrent writer added F,
|
|
// so its stale write would wipe the chunk and queue F for deletion. An
|
|
// IF_CHUNKS_EQUAL clause with no fids guards the emptiness it observed.
|
|
t.Run("empty-expectation-rejects", func(t *testing.T) {
|
|
fs, store := newServer(chunkFid(fidF))
|
|
req := &filer_pb.UpdateEntryRequest{
|
|
Directory: "/test",
|
|
Entry: &filer_pb.Entry{
|
|
Name: "obj",
|
|
Attributes: &filer_pb.FuseAttributes{Mtime: 1700000005, FileMode: 0644, Inode: 1},
|
|
},
|
|
Condition: ifChunksEqual(),
|
|
}
|
|
_, err := fs.UpdateEntry(context.Background(), req)
|
|
if status.Code(err) != codes.FailedPrecondition {
|
|
t.Fatalf("stale wipe must fail with FailedPrecondition, got %v", err)
|
|
}
|
|
if got := len(store.entries["/test/obj"].GetChunks()); got != 1 {
|
|
t.Fatalf("chunks=[F] must be preserved, got %d chunks", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
// DeleteEntry queues chunk deletions, so it must serialize on the same path
|
|
// lock the conditional writers hold; otherwise it can interleave with a
|
|
// passed precondition and the stale write resurrects the queued fids.
|
|
func TestDeleteEntryWaitsForPathLock(t *testing.T) {
|
|
store := newRenameTestStore()
|
|
store.entries["/test/obj"] = &filer.Entry{
|
|
FullPath: "/test/obj",
|
|
Attr: filer.Attr{Inode: 1, Mtime: time.Unix(1700000000, 0), Mode: 0644},
|
|
}
|
|
f := newRenameTestFiler(t, store)
|
|
fs := &FilerServer{filer: f, option: &FilerOption{}, entryLockTable: util.NewLockTable[util.FullPath]()}
|
|
|
|
lockPath := util.FullPath("/test/obj")
|
|
hold := fs.entryLockTable.AcquireLock("test", lockPath, util.ExclusiveLock)
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
fs.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{Directory: "/test", Name: "obj"})
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
t.Fatal("DeleteEntry completed while the path lock was held")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
|
|
fs.entryLockTable.ReleaseLock(lockPath, hold)
|
|
<-done
|
|
if _, ok := store.entries["/test/obj"]; ok {
|
|
t.Fatal("entry not deleted after the lock was released")
|
|
}
|
|
}
|