Files
seaweedfs/weed/filer/meta_log_collection_test.go
T
8d97284d0a filer: option to store system metadata logs in their own collection (#11551)
* feat(filer): option to store system metadata logs in their own collection

The filer's internal /topics/.system/log chunks are assigned to the
filer's default collection (-collection). In a multi-filer deployment
that default is often empty, so every restart flap, full-sync, or
event-buffered flush grows the default collection with system chunks that
are indistinguishable from user data in collection.list. This is a large
part of what makes the default collection balloon and confuses orphan
analysis.

This keeps the internal log in a dedicated collection when the operator
asks for one, without changing where user data goes:

- New optional override, filer.options.metaLog.collection (and
  .replication), read in NewFiler so both `weed filer` and
  `weed server -filer` honour it. Default "" => exactly today's
  behaviour (log follows the filer default), fully backward compatible.
- Resolution is a small helper: override first, then the filer default,
  then a storage rule matched on the log path. Kept separate from the
  user write path so the internal log targets itself.
- bucketCollection() is hardened the same way it already protects the
  filer's default collection: a bucket that happens to resolve to the
  redirected meta-log collection must not drop it on delete, because it
  backs internal log volumes.
- Scaffold filer.toml documents the new knobs under [filer.options].

Related to the persisted deletion ledger branch (fix/persist-deletion-queue):
together they cut the two sources of post-flap junk in the default
collection — that PR stops orphaned user-chunk leak on filer crash,
this one stops the internal log from living in default at all. They are
independent: no file overlap, no functional dependency; either can merge
first. They are paired only in the narrative of cleaning up default.

Adds unit tests for the collection/replication resolution chain, the
viper keys, and the bucket-delete guard (run green under -race).

Co-Authored-By: Athena 🏛️ <hermes-agent@local> (custom / Qwen3.8-Flash-Next-ROCmFP4)

* filer: collect bucket chunks when its collection survives the delete

bucketCollection returning "" preserves the collection, but the bucket
path still skipped per-entry chunk collection and could skip listing the
children entirely, so a bucket sharing the meta-log (or any preserved)
collection left its object chunks orphaned with no entry pointing at
them. Only the wholesale drop of a deleted collection skips those now.

Note in filer.toml that the meta-log target should stay stable: chunks
written under an older collection are not migrated.

* filer: exercise the metaLog override wiring through NewFiler

The viper test only echoed back the keys it set, so a wrong key in
NewFiler would still pass. It now asserts the fields NewFiler fills
from those keys.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* filer: tighten comments around the metaLog collection override

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Chris Lu <chris.lu@gmail.com>
Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-02 08:50:24 +08:00

138 lines
4.4 KiB
Go

package filer
import (
"context"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// The metadata log's collection resolution must let operators redirect the
// internal /topics/.system/log chunks into their own collection without
// touching where user data goes, and must be strictly backward compatible
// when the override is unset.
func TestMetaLogCollectionResolution(t *testing.T) {
tests := []struct {
name string
target string // filer.options.metaLog.collection override
filerDefault string // -collection flag value
ruleCollection string // storage rule matched on the log path
want string
}{
{
name: "no override: filer default wins (today's behaviour)",
target: "",
filerDefault: "mydata",
ruleCollection: "",
want: "mydata",
},
{
name: "no override anywhere: still empty (default collection)",
target: "",
filerDefault: "",
ruleCollection: "",
want: "",
},
{
name: "override wins over filer default",
target: "filer-meta",
filerDefault: "mydata",
ruleCollection: "",
want: "filer-meta",
},
{
name: "override wins over matched rule",
target: "filer-meta",
filerDefault: "",
ruleCollection: "rulecol",
want: "filer-meta",
},
{
name: "no override but rule set: rule used (unchanged fallback chain)",
target: "",
filerDefault: "",
ruleCollection: "rulecol",
want: "rulecol",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Drive the same setter a running filer would use post-construction.
f := &Filer{}
f.metaLogTargetCollection = tc.target
f.metaLogCollection = tc.filerDefault
if got := f.metaLogCollectionFor(tc.ruleCollection); got != tc.want {
t.Errorf("metaLogCollectionFor(%q) = %q, want %q", tc.ruleCollection, got, tc.want)
}
})
}
}
func TestMetaLogReplicationResolution(t *testing.T) {
f := &Filer{}
f.metaLogTargetReplication = "110"
f.metaLogReplication = "010"
if got := f.metaLogReplicationFor("001"); got != "110" {
t.Errorf("override must win, got %q", got)
}
f.metaLogTargetReplication = ""
if got := f.metaLogReplicationFor("001"); got != "010" {
t.Errorf("filer replication must win when no override, got %q", got)
}
f.metaLogReplication = ""
if got := f.metaLogReplicationFor("001"); got != "001" {
t.Errorf("rule replication must be used last, got %q", got)
}
}
// TestViperReadsMetaLogOverrides proves the exact viper keys the docs promise
// are read into the fields a running filer uses.
func TestViperReadsMetaLogOverrides(t *testing.T) {
v := util.GetViper()
v.Set("filer.options.metaLog.collection", "filer-meta")
v.Set("filer.options.metaLog.replication", "100")
defer func() {
// Reset so other tests see the unset default.
v.Set("filer.options.metaLog.collection", "")
v.Set("filer.options.metaLog.replication", "")
}()
f := NewFiler(pb.ServerDiscovery{}, nil, "", "", "", "", "", 255, nil)
if got := f.metaLogTargetCollection; got != "filer-meta" {
t.Fatalf("NewFiler did not read the collection override: %q", got)
}
if got := f.metaLogTargetReplication; got != "100" {
t.Fatalf("NewFiler did not read the replication override: %q", got)
}
}
// TestBucketCollectionKeepsMetaLogTargetCollection mirrors the guarantee that
// protects the filer's default collection: when a bucket resolves to the
// collection the system metadata log was redirected to, deleting the bucket
// must not drop that collection (it backs internal log volumes).
func TestBucketCollectionKeepsMetaLogTargetCollection(t *testing.T) {
f, store, master := newFilerWithFakeMaster(t)
// Operator redirected the meta log to its own collection...
f.metaLogTargetCollection = "filer-meta"
// ...and a bucket happens to resolve to that very collection.
f.FilerConf.SetLocationConf(&filer_pb.FilerConf_PathConf{
LocationPrefix: "/buckets/a",
Collection: "filer-meta",
})
seedBucket(t, store, util.FullPath("/buckets/a"))
if err := f.DeleteEntryMetaAndData(context.Background(), "/buckets/a", true, false, true, false, nil, 0); err != nil {
t.Fatalf("DeleteEntryMetaAndData: %v", err)
}
select {
case call := <-master.calls:
t.Fatalf("the meta-log target collection was deleted by a bucket delete: %q", call.name)
default:
}
}