From 0f718f8509c5160d41354789d830a17c3d7c8354 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sat, 25 Jul 2026 11:20:41 -0700 Subject: [PATCH] filer: add a placement overlay seam for the write path (#10437) * filer: add a placement overlay seam for the write path New volumes take their disk type, replication, and data center from the explicit request or the matched filer.conf rule. That leaves no way for a feature to steer a whole collection onto a medium without an operator writing an fs.configure rule by hand. Add a generic PlacementOverlay hook on the filer: a func that maps a collection to a placement override, installed by a factory the way the plugin-worker handlers register. detectStorageOption consults it between the explicit request value and the filer.conf rule, so it overrides the rule but yields to a value the caller asked for. The seam names no feature concepts, so it stays generic; a downstream build registers the overlay it wants (e.g. a storage-class Landing tier). Claude-Session: https://claude.ai/code/session_01Ks16jnt4S7gdDk8cheQ3xu * filer: address review on the placement overlay seam Honor ResolvePlacement's ok flag explicitly rather than relying on empty values falling through the util.Nvl chain, and log at V(4) when the overlay steers a collection. Document that RegisterPlacementOverlay is init-only, so the unsynchronized read in NewFiler cannot race the write. Claude-Session: https://claude.ai/code/session_01Ks16jnt4S7gdDk8cheQ3xu --- weed/filer/filer.go | 5 +++ weed/filer/placement_overlay.go | 36 ++++++++++++++++++++++ weed/server/filer_server_handlers_write.go | 23 +++++++++++--- 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 weed/filer/placement_overlay.go diff --git a/weed/filer/filer.go b/weed/filer/filer.go index 4595c55f4..bd349af67 100644 --- a/weed/filer/filer.go +++ b/weed/filer/filer.go @@ -56,6 +56,7 @@ type Filer struct { MetaAggregator *MetaAggregator Signature int32 FilerConf *FilerConf + placementOverlay PlacementOverlay RemoteStorage *FilerRemoteStorage lazyFetchGroup singleflight.Group lazyListGroup singleflight.Group @@ -96,6 +97,10 @@ func NewFiler(masters pb.ServerDiscovery, grpcDialOption grpc.DialOption, filerH f.metaLogCollection = collection f.metaLogReplication = replication + if newPlacementOverlay != nil { + f.placementOverlay = newPlacementOverlay(f) + } + go f.loopProcessingDeletion() return f diff --git a/weed/filer/placement_overlay.go b/weed/filer/placement_overlay.go new file mode 100644 index 000000000..9c9f80873 --- /dev/null +++ b/weed/filer/placement_overlay.go @@ -0,0 +1,36 @@ +package filer + +// PlacementOverlay resolves a write-time placement override for a collection — +// the disk type, replication, and data center new volumes of that collection +// should land on — or ok=false when the collection has none. +// +// It exists so a feature can steer where a bound collection's data lands without +// every operator setting an fs.configure rule by hand. The type is deliberately +// generic: it names no placement-policy concepts, so a downstream build +// registers whatever overlay it wants without coupling this seam to it. +type PlacementOverlay func(collection string) (diskType, replication, dataCenter string, ok bool) + +// newPlacementOverlay builds the overlay for a filer. Enterprise sets it in an +// init() pulled in by a side-effect import, the way plugin-worker handlers +// register — the setter cannot live here because this package is overwritten by +// the sync. +var newPlacementOverlay func(*Filer) PlacementOverlay + +// RegisterPlacementOverlay installs the factory. It is meant to be called from a +// package init() — before any Filer is constructed and before serving starts — +// so the unsynchronized read in NewFiler never races the write. The last +// registration wins; there is only ever one overlay. +func RegisterPlacementOverlay(factory func(*Filer) PlacementOverlay) { + newPlacementOverlay = factory +} + +// ResolvePlacement returns the overlay's placement for a collection, or +// ok=false when there is no overlay or the collection is not steered. The write +// path applies these between the explicit request value and the filer.conf +// rule, so an overlay overrides the rule but yields to an explicit request. +func (f *Filer) ResolvePlacement(collection string) (diskType, replication, dataCenter string, ok bool) { + if f == nil || f.placementOverlay == nil || collection == "" { + return "", "", "", false + } + return f.placementOverlay(collection) +} diff --git a/weed/server/filer_server_handlers_write.go b/weed/server/filer_server_handlers_write.go index f44f53004..1344c67f5 100644 --- a/weed/server/filer_server_handlers_write.go +++ b/weed/server/filer_server_handlers_write.go @@ -295,14 +295,29 @@ func (fs *FilerServer) detectStorageOption(ctx context.Context, requestURI, qCol ttlSeconds = int32(ttl.Minutes()) * 60 } + collection := util.Nvl(qCollection, rule.Collection, bucketDefaultCollection, fs.option.Collection) + + // A placement overlay steers a bound collection's new volumes onto a tier. + // It sits between the explicit request and the filer.conf rule: it overrides + // the rule but yields to a value the caller asked for outright. Only a + // steered collection contributes values; an unsteered one clears them so it + // falls straight through to the rule. + overlayDisk, overlayReplication, overlayDataCenter, overlaySteered := fs.filer.ResolvePlacement(collection) + if !overlaySteered { + overlayDisk, overlayReplication, overlayDataCenter = "", "", "" + } else { + glog.V(4).InfofCtx(ctx, "placement overlay steers collection %s: disk=%q replication=%q dataCenter=%q", + collection, overlayDisk, overlayReplication, overlayDataCenter) + } + return &operation.StorageOption{ - Replication: util.Nvl(qReplication, rule.Replication, fs.option.DefaultReplication), - Collection: util.Nvl(qCollection, rule.Collection, bucketDefaultCollection, fs.option.Collection), - DataCenter: util.Nvl(dataCenter, rule.DataCenter, fs.option.DataCenter), + Replication: util.Nvl(qReplication, overlayReplication, rule.Replication, fs.option.DefaultReplication), + Collection: collection, + DataCenter: util.Nvl(dataCenter, overlayDataCenter, rule.DataCenter, fs.option.DataCenter), Rack: util.Nvl(rack, rule.Rack, fs.option.Rack), DataNode: util.Nvl(dataNode, rule.DataNode, fs.option.DataNode), TtlSeconds: ttlSeconds, - DiskType: util.Nvl(diskType, rule.DiskType), + DiskType: util.Nvl(diskType, overlayDisk, rule.DiskType), Fsync: rule.Fsync, VolumeGrowthCount: rule.VolumeGrowthCount, MaxFileNameLength: maxFileNameLength,