mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-25 14:30:43 +01:00
* filer_pb: add MkdirExclusive A taken name leaves the callback as a result, not an error, so filer failover does not count a lost race as a filer failure. * s3tables: create the entry with its attributes in one exclusive create CreateTableBucket and CreateNamespace checked that the name was free and then wrote the entry in several calls, so a second create in the window replaced the first caller's entry including its owner. The create now carries the marker, metadata and tags in one O_EXCL CreateEntry that answers ErrEntryAlreadyExists with the same 409. * s3: PutBucket uses an exclusive mkdir The same window let a plain PutBucket replace a table bucket created concurrently. The already-exists branch now answers from the entry itself, since the registry just cached the name as absent, and the admin auto-create path is exclusive too. * s3: read the winning entry when a create loses the race The concurrent-create branch called existingBucketError on a name the registry had just cached as absent, answering NoSuchBucket, and the auto-create fallback could assign an S3 owner to a table bucket it met there. The negative cache is cleared before the existing-bucket error is built, and a table bucket keeps its owner-less shape. * s3: fail an auto-create that met a table bucket A table bucket carries no S3 owner and must not gain one, and the upload that auto-create ran for must not continue into it either. * s3: fail an auto-create when the winning entry cannot be read An unreadable existing entry fell through to success, so the write continued into an unvalidated entry, possibly a table bucket.
90 lines
3.4 KiB
Go
90 lines
3.4 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]))
|
|
}
|