Files
seaweedfs/weed/command/mini_bucket_test.go
T
Chris LuandGitHub 8a532cc0cf mini: state the format of a -tableBucket, do not infer it (#10851)
* mini: state the format of a -tableBucket, do not infer it

A table bucket holds one format and that format decides which catalog can
serve it, but the flag only took names. The format came from
miniTableBucketFormat(): Iceberg whenever its port was up, Lance only when
it was not. So -tableBucket=vectors on a default mini quietly made an
ICEBERG bucket that the Lance namespace then refused every table in, and
the only way to get a Lance one was -s3.port.iceberg=0, which buys it by
deleting the other catalog. One flag, two meanings, decided by an unrelated
port.

Each entry is now name[:FORMAT], unsuffixed meaning ICEBERG as before:

    weed mini -tableBucket=warehouse,vectors:LANCE

Both catalogs stay up and both buckets are reachable. A name whose format
has no endpoint here is skipped with a warning rather than created out of
reach, and the Iceberg-only S3_TABLE_BUCKET default-routing hint gets the
Iceberg names alone, without their suffixes.

* mini: do not reuse a table bucket that holds another format

CreateTableBucket answers BucketAlreadyExists on the name alone, so
-tableBucket=vectors against a bucket created as LANCE logged "already
exists" and moved on, and the Iceberg default-warehouse hint then pointed
at it. Every table create against that catalog fails with "table bucket
vectors holds LANCE tables", far from the flag that chose it.

ensureMiniTableBuckets now reads the format of a bucket it did not create,
warns when it is not the one asked for, and returns only the buckets that
hold what was requested. S3_TABLE_BUCKET is seeded from that list, so an
unprefixed Iceberg request falls back to its own default rather than
committing into a Lance bucket. A bucket predating declared formats reports
an empty one and still accepts either.

* mini: normalize S3_TABLE_BUCKET whichever way the spec arrived

The rewrite that keeps Lance names out of the Iceberg default warehouse only
ran when the flag supplied the spec. Set the variable directly, as the docker
quickstart does, and it reached the catalog untouched: S3_TABLE_BUCKET=
vectors:LANCE,warehouse made the unprefixed default the literal string
"vectors:LANCE", a bucket no lookup finds, while warehouse sat behind it.

The variable is both mini's input and the catalog's routing hint, so it is
now always rewritten from the buckets that came back holding Iceberg tables,
and unset when there are none rather than left pointing somewhere stale.

* mini: reuse a table bucket only when its format reads back

An ordinary S3 bucket wearing the name answers CreateTableBucket with the
same BucketAlreadyExists as a table bucket does, and the format lookup that
follows returned "" for a failed read exactly as it does for a bucket
predating declared formats. So -bucket=data -tableBucket=data reported
nothing and published data as the Iceberg default warehouse, where every
unprefixed request 404s on a bucket that is not a catalog.

The lookup now returns its error, and only a bucket that reads back as the
format asked for is reused. Anything else is left alone with a warning
naming why, rather than routed to and discovered later.
2026-08-20 23:46:32 -07:00

124 lines
4.7 KiB
Go

package command
import (
"reflect"
"testing"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
)
func TestParseBucketList(t *testing.T) {
tests := []struct {
name string
in string
want []string
}{
{"empty", "", nil},
{"single", "one", []string{"one"}},
{"multi", "one,two,three", []string{"one", "two", "three"}},
{"trims whitespace", " one , two , three ", []string{"one", "two", "three"}},
{"drops empty entries", "one,,two,", []string{"one", "two"}},
{"dedupes preserving order", "one,two,one,three,two", []string{"one", "two", "three"}},
{"only commas", ",,,", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseBucketList(tt.in)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseBucketList(%q) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
func TestParseTableBucketList(t *testing.T) {
tests := []struct {
name string
in string
want []tableBucketEntry
}{
{"empty", "", nil},
{"defaults to iceberg", "warehouse", []tableBucketEntry{{"warehouse", s3tables.FormatIceberg}}},
{"explicit format", "vectors:LANCE", []tableBucketEntry{{"vectors", s3tables.FormatLance}}},
{"format is case insensitive", "vectors:lance", []tableBucketEntry{{"vectors", s3tables.FormatLance}}},
{"mixed formats", "warehouse,vectors:LANCE", []tableBucketEntry{{"warehouse", s3tables.FormatIceberg}, {"vectors", s3tables.FormatLance}}},
{"trims whitespace", " warehouse , vectors : LANCE ", []tableBucketEntry{{"warehouse", s3tables.FormatIceberg}, {"vectors", s3tables.FormatLance}}},
{"drops unsupported format", "warehouse,vectors:DELTA", []tableBucketEntry{{"warehouse", s3tables.FormatIceberg}}},
{"empty format is the default", "warehouse:", []tableBucketEntry{{"warehouse", s3tables.FormatIceberg}}},
{"dedupes by name", "vectors:LANCE,vectors:ICEBERG", []tableBucketEntry{{"vectors", s3tables.FormatLance}}},
{"drops empty entries", "one,,two:LANCE,", []tableBucketEntry{{"one", s3tables.FormatIceberg}, {"two", s3tables.FormatLance}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseTableBucketList(tt.in)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseTableBucketList(%q) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
// A bucket's format decides which endpoint has to be up for it, so turning one
// catalog off must not change what the other format means.
func TestMiniServesTableFormat(t *testing.T) {
icebergPort, lancePort := *miniS3Options.portIceberg, *miniS3Options.portLance
t.Cleanup(func() {
*miniS3Options.portIceberg, *miniS3Options.portLance = icebergPort, lancePort
})
*miniS3Options.portIceberg, *miniS3Options.portLance = 8181, 9101
if !miniServesTableFormat(s3tables.FormatIceberg) || !miniServesTableFormat(s3tables.FormatLance) {
t.Errorf("both endpoints up: want both formats served")
}
*miniS3Options.portIceberg = 0
if miniServesTableFormat(s3tables.FormatIceberg) {
t.Errorf("iceberg port 0: want ICEBERG unserved")
}
if !miniServesTableFormat(s3tables.FormatLance) {
t.Errorf("iceberg port 0: want LANCE still served")
}
*miniS3Options.portLance = 0
if miniServesTableFormat(s3tables.FormatLance) {
t.Errorf("lance port 0: want LANCE unserved")
}
if miniServesTableFormat("DELTA") {
t.Errorf("unknown format: want unserved")
}
}
// The Iceberg catalog looks its default warehouse up by name, so a Lance entry
// or a leftover :FORMAT suffix in S3_TABLE_BUCKET names a bucket it cannot find.
func TestIcebergRoutingNames(t *testing.T) {
tests := []struct {
name string
in []tableBucketEntry
want []string
}{
{"none", nil, nil},
{"iceberg only", []tableBucketEntry{{"warehouse", s3tables.FormatIceberg}}, []string{"warehouse"}},
{"lance is not routable", []tableBucketEntry{{"vectors", s3tables.FormatLance}}, nil},
{"lance first", []tableBucketEntry{{"vectors", s3tables.FormatLance}, {"warehouse", s3tables.FormatIceberg}}, []string{"warehouse"}},
{"order preserved", []tableBucketEntry{{"raw", s3tables.FormatIceberg}, {"vectors", s3tables.FormatLance}, {"curated", s3tables.FormatIceberg}}, []string{"raw", "curated"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := icebergRoutingNames(tt.in)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("icebergRoutingNames(%v) = %v, want %v", tt.in, got, tt.want)
}
})
}
}
// A spec arriving through S3_TABLE_BUCKET carries suffixes too, and the catalog
// reads that same variable, so parsing and routing have to agree on the name.
func TestIcebergRoutingNamesFromEnvSpec(t *testing.T) {
got := icebergRoutingNames(parseTableBucketList("vectors:LANCE,warehouse"))
if want := []string{"warehouse"}; !reflect.DeepEqual(got, want) {
t.Errorf("routing names for env spec = %v, want %v", got, want)
}
}