mirror of
https://github.com/hanzoai/s3.git
synced 2026-11-02 13:11:24 +00:00
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.
- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
"Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
(replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
keep their upstream import paths with zero churn. Internal identifiers and
import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
help/usage/flag strings, shell autocomplete program name, and the product name
in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
(e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
`cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
updated to ignore the `s3` build output.
211 lines
5.8 KiB
Go
211 lines
5.8 KiB
Go
package integration
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/test/kafka/internal/testutil"
|
|
)
|
|
|
|
// TestSchemaRegistryEventualConsistency reproduces the issue where schemas
|
|
// are registered successfully but are not immediately queryable due to
|
|
// Schema Registry's consumer lag
|
|
func TestSchemaRegistryEventualConsistency(t *testing.T) {
|
|
// This test requires real SMQ backend
|
|
gateway := testutil.NewGatewayTestServerWithSMQ(t, testutil.SMQRequired)
|
|
defer gateway.CleanupAndClose()
|
|
|
|
addr := gateway.StartAndWait()
|
|
t.Logf("Gateway running on %s", addr)
|
|
|
|
// Schema Registry URL from environment or default
|
|
schemaRegistryURL := "http://localhost:8081"
|
|
|
|
// Wait for Schema Registry to be ready
|
|
if !waitForSchemaRegistry(t, schemaRegistryURL, 30*time.Second) {
|
|
t.Fatal("Schema Registry not ready")
|
|
}
|
|
|
|
// Define test schemas
|
|
valueSchema := `{"type":"record","name":"TestMessage","fields":[{"name":"id","type":"string"}]}`
|
|
keySchema := `{"type":"string"}`
|
|
|
|
// Register multiple schemas rapidly (simulates the load test scenario)
|
|
subjects := []string{
|
|
"test-topic-0-value",
|
|
"test-topic-0-key",
|
|
"test-topic-1-value",
|
|
"test-topic-1-key",
|
|
"test-topic-2-value",
|
|
"test-topic-2-key",
|
|
"test-topic-3-value",
|
|
"test-topic-3-key",
|
|
}
|
|
|
|
t.Log("Registering schemas rapidly...")
|
|
registeredIDs := make(map[string]int)
|
|
for _, subject := range subjects {
|
|
schema := valueSchema
|
|
if strings.HasSuffix(subject, "-key") {
|
|
schema = keySchema
|
|
}
|
|
|
|
id, err := registerSchema(schemaRegistryURL, subject, schema)
|
|
if err != nil {
|
|
t.Fatalf("Failed to register schema for %s: %v", subject, err)
|
|
}
|
|
registeredIDs[subject] = id
|
|
t.Logf("Registered %s with ID %d", subject, id)
|
|
}
|
|
|
|
t.Log("All schemas registered successfully!")
|
|
|
|
// Now immediately try to verify them (this reproduces the bug)
|
|
t.Log("Immediately verifying schemas (without delay)...")
|
|
immediateFailures := 0
|
|
for _, subject := range subjects {
|
|
exists, id, version, err := verifySchema(schemaRegistryURL, subject)
|
|
if err != nil || !exists {
|
|
immediateFailures++
|
|
t.Logf("Immediate verification failed for %s: exists=%v id=%d err=%v", subject, exists, id, err)
|
|
} else {
|
|
t.Logf("Immediate verification passed for %s: ID=%d Version=%d", subject, id, version)
|
|
}
|
|
}
|
|
|
|
if immediateFailures > 0 {
|
|
t.Logf("BUG REPRODUCED: %d/%d schemas not immediately queryable after registration",
|
|
immediateFailures, len(subjects))
|
|
t.Logf(" This is due to Schema Registry's KafkaStoreReaderThread lag")
|
|
}
|
|
|
|
// Now verify with retry logic (this should succeed)
|
|
t.Log("Verifying schemas with retry logic...")
|
|
for _, subject := range subjects {
|
|
expectedID := registeredIDs[subject]
|
|
if !verifySchemaWithRetry(t, schemaRegistryURL, subject, expectedID, 5*time.Second) {
|
|
t.Errorf("Failed to verify %s even with retry", subject)
|
|
}
|
|
}
|
|
|
|
t.Log("✓ All schemas verified successfully with retry logic!")
|
|
}
|
|
|
|
// registerSchema registers a schema and returns its ID
|
|
func registerSchema(registryURL, subject, schema string) (int, error) {
|
|
// Escape the schema JSON
|
|
escapedSchema, err := json.Marshal(schema)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
payload := fmt.Sprintf(`{"schema":%s,"schemaType":"AVRO"}`, escapedSchema)
|
|
|
|
resp, err := http.Post(
|
|
fmt.Sprintf("%s/subjects/%s/versions", registryURL, subject),
|
|
"application/vnd.schemaregistry.v1+json",
|
|
strings.NewReader(payload),
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return 0, fmt.Errorf("registration failed: %s - %s", resp.Status, string(body))
|
|
}
|
|
|
|
var result struct {
|
|
ID int `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return result.ID, nil
|
|
}
|
|
|
|
// verifySchema checks if a schema exists
|
|
func verifySchema(registryURL, subject string) (exists bool, id int, version int, err error) {
|
|
resp, err := http.Get(fmt.Sprintf("%s/subjects/%s/versions/latest", registryURL, subject))
|
|
if err != nil {
|
|
return false, 0, 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return false, 0, 0, nil
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return false, 0, 0, fmt.Errorf("verification failed: %s - %s", resp.Status, string(body))
|
|
}
|
|
|
|
var result struct {
|
|
ID int `json:"id"`
|
|
Version int `json:"version"`
|
|
Schema string `json:"schema"`
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
return false, 0, 0, err
|
|
}
|
|
|
|
return true, result.ID, result.Version, nil
|
|
}
|
|
|
|
// verifySchemaWithRetry verifies a schema with retry logic
|
|
func verifySchemaWithRetry(t *testing.T, registryURL, subject string, expectedID int, timeout time.Duration) bool {
|
|
deadline := time.Now().Add(timeout)
|
|
attempt := 0
|
|
|
|
for time.Now().Before(deadline) {
|
|
attempt++
|
|
exists, id, version, err := verifySchema(registryURL, subject)
|
|
|
|
if err == nil && exists && id == expectedID {
|
|
if attempt > 1 {
|
|
t.Logf("✓ %s verified after %d attempts (ID=%d, Version=%d)", subject, attempt, id, version)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Wait before retry (exponential backoff)
|
|
waitTime := time.Duration(attempt*100) * time.Millisecond
|
|
if waitTime > 1*time.Second {
|
|
waitTime = 1 * time.Second
|
|
}
|
|
time.Sleep(waitTime)
|
|
}
|
|
|
|
t.Logf("%s verification timed out after %d attempts", subject, attempt)
|
|
return false
|
|
}
|
|
|
|
// waitForSchemaRegistry waits for Schema Registry to be ready
|
|
func waitForSchemaRegistry(t *testing.T, url string, timeout time.Duration) bool {
|
|
deadline := time.Now().Add(timeout)
|
|
|
|
for time.Now().Before(deadline) {
|
|
resp, err := http.Get(url + "/subjects")
|
|
if err == nil && resp.StatusCode == http.StatusOK {
|
|
resp.Body.Close()
|
|
return true
|
|
}
|
|
if resp != nil {
|
|
resp.Body.Close()
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
return false
|
|
}
|