filer: skip UpdateEntry when inline content is unchanged (#11580)

* filer: skip UpdateEntry when inline content is unchanged

SaveInsideFiler rewrites config files (IAM identities, filer.conf,
remote mappings, policies) unconditionally. Each no-op UpdateEntry is a
metadata event the local meta log persists to /topics/.system/log,
which appends a chunk to a volume. A client that rewrites identical
config on a timer, e.g. the seaweedfs-operator 5-minute resync calling
UpdateUser with unchanged actions, keeps .dat/.idx files growing on an
otherwise idle cluster and prevents HDD spindown
(seaweedfs/seaweedfs#11571).

Skip the UpdateEntry when the stored inline content is byte-identical,
so unchanged writes produce no metadata event and no volume writes.

* filer: test that identical SaveInsideFiler writes skip UpdateEntry

* filer: require stamped Md5 before skipping identical writes

An entry holding identical content but no Md5 (written before hashing,
or by a tool that cleared it) would never get the stamp that
IF_ETAG_MATCH conditional writes key off. Skip only when both the
stored content and its Md5 match, so one write still lands to repair
the stamp.
This commit is contained in:
Chris Lu authored and GitHub committed 2026-10-03 21:00:51 +08:00
1 parent 562afa8ec9
commit eafe79ebff
3 files changed
+58 -2

No files matched your search

+4 -2
View File
@@ -262,8 +262,9 @@ func TestClearReadOnly(t *testing.T) {
type fakeFilerConfClient struct {
filer_pb.SeaweedFilerClient
mu sync.Mutex
entries map[string]*filer_pb.Entry // key: dir+"/"+name
mu sync.Mutex
entries map[string]*filer_pb.Entry // key: dir+"/"+name
updateCalls int
}
func newFakeFilerConfClient() *fakeFilerConfClient {
@@ -310,6 +311,7 @@ func (c *fakeFilerConfClient) CreateEntry(_ context.Context, in *filer_pb.Create
func (c *fakeFilerConfClient) UpdateEntry(_ context.Context, in *filer_pb.UpdateEntryRequest, _ ...grpc.CallOption) (*filer_pb.UpdateEntryResponse, error) {
c.mu.Lock()
defer c.mu.Unlock()
c.updateCalls++
key := c.key(in.Directory, in.Entry.Name)
if !conditionHoldsLocked(in.Condition, c.entries[key]) {
return nil, errors.New("precondition failed")
+3
View File
@@ -75,6 +75,9 @@ func SaveInsideFiler(ctx context.Context, client filer_pb.SeaweedFilerClient, di
})
} else if err == nil {
entry := resp.Entry
if bytes.Equal(entry.Content, content) && bytes.Equal(entry.GetAttributes().GetMd5(), contentMd5[:]) {
return nil
}
entry.Content = content
entry.Attributes.Mtime = time.Now().Unix()
entry.Attributes.FileSize = uint64(len(content))
+51
View File
@@ -0,0 +1,51 @@
package filer
import (
"context"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Rewriting identical inline content must not issue UpdateEntry: each one is a
// metadata event the local meta log persists to /topics/.system/log, and a
// client that rewrites the same config on a timer (e.g. the operator's 5-minute
// resync) keeps the volumes growing on an otherwise idle cluster.
func TestSaveInsideFilerSkipsIdenticalContent(t *testing.T) {
ctx := context.Background()
client := newFakeFilerConfClient()
require.NoError(t, SaveInsideFiler(ctx, client, DirectoryEtcSeaweedFS, "a.json", []byte("v1")))
require.Equal(t, 0, client.updateCalls)
require.NoError(t, SaveInsideFiler(ctx, client, DirectoryEtcSeaweedFS, "a.json", []byte("v1")))
assert.Equal(t, 0, client.updateCalls)
require.NoError(t, SaveInsideFiler(ctx, client, DirectoryEtcSeaweedFS, "a.json", []byte("v2")))
assert.Equal(t, 1, client.updateCalls)
content, err := ReadInsideFiler(ctx, client, DirectoryEtcSeaweedFS, "a.json")
require.NoError(t, err)
assert.Equal(t, []byte("v2"), content)
}
// A legacy entry holding identical bytes but no Md5 still gets one write to
// stamp the hash that IF_ETAG_MATCH conditional writes key off; later
// identical writes skip.
func TestSaveInsideFilerStampsMd5OnLegacyEntry(t *testing.T) {
ctx := context.Background()
client := newFakeFilerConfClient()
client.entries[client.key(DirectoryEtcSeaweedFS, "b.json")] = &filer_pb.Entry{
Name: "b.json",
Content: []byte("v1"),
Attributes: &filer_pb.FuseAttributes{},
}
require.NoError(t, SaveInsideFiler(ctx, client, DirectoryEtcSeaweedFS, "b.json", []byte("v1")))
assert.Equal(t, 1, client.updateCalls)
require.NoError(t, SaveInsideFiler(ctx, client, DirectoryEtcSeaweedFS, "b.json", []byte("v1")))
assert.Equal(t, 1, client.updateCalls)
}