mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-02 18:22:40 +01:00
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:
1 parent
e2826c48bd
commit
ab355b43b9
2 files changed
+35
-44
No files matched your search
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// VectorIndexNamespace holds a vector bucket's indexes. Only the S3 Vectors
|
||||
@@ -53,9 +54,9 @@ func (h *S3TablesHandler) checkVectorNamespaceWrite(ctx context.Context, filerCl
|
||||
|
||||
// fenceVectorNamespace returns a fence for an untrusted write naming the
|
||||
// vector namespace of a LANCE bucket. The request still runs, so the
|
||||
// operation's own checks answer everything short of a write and an ungranted
|
||||
// caller gets its usual denial. A CreateTable of an index that already exists
|
||||
// is the vector engine's declare and passes unfenced.
|
||||
// operation's own checks answer everything short of a write: an ungranted
|
||||
// caller gets its usual denial, and a CreateTable that finds the index already
|
||||
// there (the vector engine's declare) succeeds.
|
||||
func (h *S3TablesHandler) fenceVectorNamespace(w http.ResponseWriter, r *http.Request, filerClient FilerClient, operation string) *vectorNamespaceFence {
|
||||
if h.trusted || strings.HasPrefix(operation, "Get") || strings.HasPrefix(operation, "List") {
|
||||
return nil
|
||||
@@ -69,7 +70,6 @@ func (h *S3TablesHandler) fenceVectorNamespace(w http.ResponseWriter, r *http.Re
|
||||
SourceNamespace []string `json:"sourceNamespace"`
|
||||
DestNamespace []string `json:"destNamespace"`
|
||||
ResourceARN string `json:"resourceArn"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
// Like the handler's own decode, this keeps going past a mistyped field.
|
||||
_ = json.Unmarshal(body, &req)
|
||||
@@ -83,34 +83,13 @@ func (h *S3TablesHandler) fenceVectorNamespace(w http.ResponseWriter, r *http.Re
|
||||
targets = append(targets, target{bucket, []string{namespace}})
|
||||
}
|
||||
for _, t := range targets {
|
||||
if t.bucket == "" || h.checkVectorNamespaceWrite(r.Context(), filerClient, t.bucket, t.namespace) == nil {
|
||||
continue
|
||||
if t.bucket != "" && h.checkVectorNamespaceWrite(r.Context(), filerClient, t.bucket, t.namespace) != nil {
|
||||
return &vectorNamespaceFence{ResponseWriter: w, h: h, filer: filerClient}
|
||||
}
|
||||
if operation == "CreateTable" && isVectorIndexNamespace(req.Namespace) && h.vectorIndexExists(r.Context(), filerClient, bucketName, req.Name) {
|
||||
return nil
|
||||
}
|
||||
return &vectorNamespaceFence{ResponseWriter: w, h: h, filer: filerClient}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *S3TablesHandler) vectorIndexExists(ctx context.Context, filerClient FilerClient, bucketName, name string) bool {
|
||||
if _, err := validateTableName(name); err != nil {
|
||||
return false
|
||||
}
|
||||
exists := false
|
||||
_ = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
entry, err := h.lookupEntry(ctx, client, GetTablePath(bucketName, VectorIndexNamespace, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, hasMetadata := entry.Extended[ExtendedKeyMetadata]
|
||||
exists = hasMetadata && !isUnfinishedCreate(entry.Extended) && EntryType(entry.Extended) == EntryTypeTable
|
||||
return nil
|
||||
})
|
||||
return exists
|
||||
}
|
||||
|
||||
// vectorNamespaceFence passes reads, refuses filer writes, and answers the
|
||||
// request as read-only once a write was refused. Handlers write only after
|
||||
// their permission check, so an ungranted caller never sees that answer.
|
||||
@@ -169,7 +148,16 @@ func (c readOnlyFilerClient) CreateEntry(context.Context, *filer_pb.CreateEntryR
|
||||
return nil, c.refuse()
|
||||
}
|
||||
|
||||
func (c readOnlyFilerClient) UpdateEntry(context.Context, *filer_pb.UpdateEntryRequest, ...grpc.CallOption) (*filer_pb.UpdateEntryResponse, error) {
|
||||
// UpdateEntry answers a rewrite that leaves the entry exactly as stored
|
||||
// without writing, so an idempotent path that repairs state already in place
|
||||
// still passes.
|
||||
func (c readOnlyFilerClient) UpdateEntry(ctx context.Context, req *filer_pb.UpdateEntryRequest, opts ...grpc.CallOption) (*filer_pb.UpdateEntryResponse, error) {
|
||||
if req.GetEntry() != nil {
|
||||
resp, err := c.SeaweedFilerClient.LookupDirectoryEntry(ctx, &filer_pb.LookupDirectoryEntryRequest{Directory: req.Directory, Name: req.Entry.Name}, opts...)
|
||||
if err == nil && proto.Equal(resp.GetEntry(), req.Entry) {
|
||||
return &filer_pb.UpdateEntryResponse{}, nil
|
||||
}
|
||||
}
|
||||
return nil, c.refuse()
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user