mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 16:28:30 +01:00
* s3tables: create a missing directory exclusively in ensureDirectory ensureDirectory looked the path up and, when it was missing, created it with a plain CreateEntry. A table create that lost the lookup race then replaced the directory another create had made, claimed and confirmed since, erasing that table's record, so a third create could claim the name and win as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * iceberg: create missing metadata parent directories exclusively saveMetadataBlob created each missing directory on the way to the metadata directory with a plain CreateEntry after a lookup. A table directory another create had made and written its record onto since the lookup was replaced by a bare directory, erasing that record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * iceberg: create missing stage-create marker directories exclusively The marker path starts at the table bucket directory, which carries the bucket record; a plain create after a missed lookup replaces a directory another writer made in between, with whatever it carries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
4.0 KiB
Go
103 lines
4.0 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables/s3tablestest"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
// competingCreateClient lands a foreign entry where the handler's existence
|
|
// check just answered "not there": the create that follows must lose to it
|
|
// rather than replace it, the way a second caller's create wins the window in
|
|
// a check-then-create race.
|
|
type competingCreateClient struct {
|
|
filer_pb.SeaweedFilerClient
|
|
f *s3tablestest.MemFiler
|
|
dir string
|
|
name string
|
|
plant bool
|
|
}
|
|
|
|
func (c *competingCreateClient) LookupDirectoryEntry(ctx context.Context, req *filer_pb.LookupDirectoryEntryRequest, opts ...grpc.CallOption) (*filer_pb.LookupDirectoryEntryResponse, error) {
|
|
resp, err := c.SeaweedFilerClient.LookupDirectoryEntry(ctx, req, opts...)
|
|
if c.plant && err != nil && req.Directory == c.dir && req.Name == c.name {
|
|
c.plant = false
|
|
c.f.Put(c.dir, c.name, map[string][]byte{
|
|
ExtendedKeyTableBucket: []byte("true"),
|
|
ExtendedKeyMetadata: []byte(`{"OwnerAccountID":"competitor"}`),
|
|
})
|
|
}
|
|
return resp, err
|
|
}
|
|
|
|
func runHandler(t *testing.T, m *Manager, client filer_pb.SeaweedFilerClient, operation string, input interface{}) (int, S3TablesError) {
|
|
t.Helper()
|
|
body, err := json.Marshal(input)
|
|
require.NoError(t, err)
|
|
req, err := newManagerRequest(context.Background(), operation, body, "admin")
|
|
require.NoError(t, err)
|
|
recorder := httptest.NewRecorder()
|
|
m.handler.HandleRequest(recorder, req, NewManagerClient(client))
|
|
result := recorder.Result()
|
|
defer result.Body.Close()
|
|
var response S3TablesError
|
|
json.NewDecoder(result.Body).Decode(&response)
|
|
return result.StatusCode, response
|
|
}
|
|
|
|
func TestCreateTableBucketLostRaceKeepsTheFirstCreate(t *testing.T) {
|
|
fs := s3tablestest.Start(t)
|
|
m := NewManager()
|
|
client := &competingCreateClient{SeaweedFilerClient: fs.Client, f: fs, dir: TablesPath, name: "bucket", plant: true}
|
|
|
|
status, resp := runHandler(t, m, client, "CreateTableBucket", &CreateTableBucketRequest{Name: "bucket"})
|
|
assert.Equal(t, http.StatusConflict, status)
|
|
assert.Equal(t, ErrCodeBucketAlreadyExists, resp.Type)
|
|
|
|
survivor := fs.Get(TablesPath, "bucket")
|
|
require.NotNil(t, survivor)
|
|
assert.Equal(t, `{"OwnerAccountID":"competitor"}`, string(survivor.Extended[ExtendedKeyMetadata]))
|
|
}
|
|
|
|
func TestCreateNamespaceLostRaceKeepsTheFirstCreate(t *testing.T) {
|
|
fs := s3tablestest.Start(t)
|
|
m := NewManager()
|
|
bucketMetadata, _ := json.Marshal(&tableBucketMetadata{Name: "bucket", OwnerAccountID: DefaultAccountID})
|
|
fs.Put(TablesPath, "bucket", map[string][]byte{ExtendedKeyTableBucket: []byte("true"), ExtendedKeyMetadata: bucketMetadata})
|
|
|
|
arn, err := BuildBucketARN(DefaultRegion, DefaultAccountID, "bucket")
|
|
require.NoError(t, err)
|
|
client := &competingCreateClient{SeaweedFilerClient: fs.Client, f: fs, dir: GetTableBucketPath("bucket"), name: "ns", plant: true}
|
|
status, resp := runHandler(t, m, client, "CreateNamespace", &CreateNamespaceRequest{
|
|
TableBucketARN: arn,
|
|
Namespace: []string{"ns"},
|
|
})
|
|
assert.Equal(t, http.StatusConflict, status)
|
|
assert.Equal(t, ErrCodeNamespaceAlreadyExists, resp.Type)
|
|
|
|
survivor := fs.Get(GetTableBucketPath("bucket"), "ns")
|
|
require.NotNil(t, survivor)
|
|
assert.Equal(t, `{"OwnerAccountID":"competitor"}`, string(survivor.Extended[ExtendedKeyMetadata]))
|
|
}
|
|
|
|
func TestEnsureDirectoryKeepsADirectoryCreatedSinceTheLookup(t *testing.T) {
|
|
fs := s3tablestest.Start(t)
|
|
tablePath := GetTablePath("bucket", "ns", "orders")
|
|
dir, name := splitPath(tablePath)
|
|
client := &competingCreateClient{SeaweedFilerClient: fs.Client, f: fs, dir: dir, name: name, plant: true}
|
|
|
|
require.NoError(t, NewS3TablesHandler().ensureDirectory(context.Background(), client, tablePath))
|
|
|
|
survivor := fs.Get(dir, name)
|
|
require.NotNil(t, survivor)
|
|
assert.Equal(t, `{"OwnerAccountID":"competitor"}`, string(survivor.Extended[ExtendedKeyMetadata]))
|
|
}
|