s3tables: keep the vector-namespace fence on a declare of an existing index (#12293)

s3tables: keep the fence on a declare of an existing index

Deciding the existing-index exception before the handler ran left a
window: an index deleted between that lookup and the handler's own check
would be recreated, unfenced, by a caller without s3vectors:CreateIndex.

Drop the up-front exception, so every CreateTable runs fenced and a
fresh create is refused at its first write. The engine's declare still
passes because the idempotent path writes nothing; for a build whose
idempotent path re-applies state already in place, the fence answers an
UpdateEntry that leaves the entry exactly as stored without writing.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris LuandClaude Opus 5.5 authored and GitHub committed 2026-10-31 16:07:31 -07:00
1 parent e2826c48bd
commit ab355b43b9
2 files changed
+35 -44

No files matched your search

+19 -16
View File
@@ -5,6 +5,7 @@ import (
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables/s3tablestest"
"github.com/stretchr/testify/assert"
@@ -264,24 +265,26 @@ func TestAuthorizeCreateTableRefusesVectorNamespace(t *testing.T) {
assert.NoError(t, untrustedManager().AuthorizeCreateTable(ctx, NewManagerClient(icebergFS.Client), req, "reader"))
}
// Only a CreateTable that finds a finished index passes unfenced, so the
// declare keeps working even where the idempotent path writes.
func TestVectorNamespaceFenceSparesOnlyExistingIndexCreate(t *testing.T) {
// Behind the fence a rewrite that leaves the entry as stored, as an
// idempotent repair does, passes without writing; any real change is refused.
func TestVectorNamespaceFencePassesOnlyNoOpUpdates(t *testing.T) {
fs := s3tablestest.Start(t)
seedVectorBucket(t, fs, FormatLance)
fs.Put(GetNamespacePath(vectorTestBucket, VectorIndexNamespace), "pending", map[string][]byte{
ExtendedKeyMetadata: []byte(`{"name":"pending","format":"LANCE"}`),
ExtendedKeyDeleting: []byte(createClaim),
})
h := untrustedManager().handler
fence := &vectorNamespaceFence{h: h, filer: NewManagerClient(fs.Client)}
idxPath := GetTablePath(vectorTestBucket, VectorIndexNamespace, "idx")
stored := fs.Get(GetNamespacePath(vectorTestBucket, VectorIndexNamespace), "idx").Extended[ExtendedKeyMetadata]
for name, fenced := range map[string]bool{"idx": false, "fresh": true, "pending": true} {
t.Run(name, func(t *testing.T) {
body := `{"tableBucketARN":"` + vectorTestARN(t) + `","namespace":["vector"],"name":"` + name + `","format":"LANCE"}`
req, err := newManagerRequest(context.Background(), "CreateTable", []byte(body), "reader")
require.NoError(t, err)
got := h.fenceVectorNamespace(nil, req, NewManagerClient(fs.Client), "CreateTable")
assert.Equal(t, fenced, got != nil)
})
}
err := fence.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
return h.setExtendedAttribute(context.Background(), client, idxPath, ExtendedKeyMetadata, stored)
})
require.NoError(t, err)
assert.False(t, fence.tripped.Load())
err = fence.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
return h.setExtendedAttribute(context.Background(), client, idxPath, ExtendedKeyMetadata, []byte(`{}`))
})
assert.ErrorIs(t, err, errVectorNamespaceReadOnly)
assert.True(t, fence.tripped.Load())
assert.Equal(t, stored, fs.Get(GetNamespacePath(vectorTestBucket, VectorIndexNamespace), "idx").Extended[ExtendedKeyMetadata])
}