mirror of
https://github.com/hanzoai/s3.git
synced 2026-10-30 14:59:09 +00:00
filer, master, iam, plugin and worker no longer use google.golang.org/grpc on
the client path.
- gRPC-free client interfaces: the generated *_grpc.pb.go client interfaces drop
"opts ...grpc.CallOption" (no caller ever passed one) and their streaming
methods return the shared seam types in s3/pb/rpc (ServerStream[T] /
BidiStream[Req,Resp]) instead of grpc.ServerStreamingClient/BidiStreamingClient.
The gRPC server scaffolding (Unimplemented*Server, Register*Server, ServiceDesc,
handler trampolines) is deleted; servers serve over the ZAP wire Dispatch.
- The ZAP adapters (filerzap/masterzap/...) implement the gRPC-free interfaces;
grpc.DialOption becomes pb.DialOption (the dial is the ZAP transport).
- Unary calls honor context deadlines: the wire client threads ctx to
transport.Conn.CallContext (v1.8.1), so a call to an unresponsive peer aborts
on the caller's deadline instead of blocking forever.
- Error semantics: the filer tags each failure with its code name ("NotFound: …",
"Internal: …") in the message, which crosses the ZAP wire and is classified by
mount/error_classifier.go — replacing grpc status/codes.
- master ZAP listener port: grpcPort+10000 could exceed 65535; made overflow-safe.
- test doubles updated to the gRPC-free interfaces (0 tests skipped).
- dead cmd/s3-db and cmd/s3-sql (postgres-wire / SQL-engine entry points whose
backends were already ripped) deleted; finished the backend-rip import cleanup.
go build ./... green; go test ./s3/... green (heavy stream benches run under
their existing -short gates).
Still on gRPC (next wave): volume_server + mq (reverted — servers/adapters not
ready), the remaining ~23 plumbing files, and the go.mod dependency itself.
267 lines
9.6 KiB
Go
267 lines
9.6 KiB
Go
package erasure_coding
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/hanzoai/s3/s3/pb"
|
|
"github.com/hanzoai/s3/s3/pb/master_pb"
|
|
"github.com/hanzoai/s3/s3/shell"
|
|
"github.com/hanzoai/s3/s3/storage/needle"
|
|
)
|
|
|
|
// TestMultiDiskECBalanceNoShardLoss is the end-to-end regression for issue 9593.
|
|
// It runs a real cluster of multi-disk volume servers (3 servers x 4 disks),
|
|
// EC-encodes a volume, then runs ec.balance, asserting hard invariants the older
|
|
// integration tests only logged:
|
|
//
|
|
// - after encode the full set of 14 EC shards exists,
|
|
// - ec.balance never loses a shard (still 14 distinct shards afterwards),
|
|
// - shards end up spread across more than one disk per node, and
|
|
// - cluster.status counts physical disks (not one per node) and matches the
|
|
// real on-disk distribution.
|
|
func TestMultiDiskECBalanceNoShardLoss(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping multi-disk EC integration test in short mode")
|
|
}
|
|
|
|
testDir := t.TempDir()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 240*time.Second)
|
|
defer cancel()
|
|
|
|
cluster, err := startMultiDiskCluster(ctx, testDir)
|
|
require.NoError(t, err)
|
|
defer cluster.Stop()
|
|
|
|
require.NoError(t, waitForServer("127.0.0.1:9334", 30*time.Second))
|
|
for i := 0; i < 3; i++ {
|
|
require.NoError(t, waitForServer(fmt.Sprintf("127.0.0.1:809%d", i), 30*time.Second))
|
|
}
|
|
t.Log("waiting for multi-disk volume servers to register...")
|
|
time.Sleep(10 * time.Second)
|
|
|
|
commandEnv := shell.NewCommandEnv(&shell.ShellOptions{
|
|
Masters: stringPtr("127.0.0.1:9334"),
|
|
GrpcDialOption: pb.DialOption{},
|
|
FilerGroup: stringPtr("default"),
|
|
})
|
|
connectToMasterAndSync(ctx, t, commandEnv)
|
|
|
|
// Upload enough small files that the volume holds real data to encode.
|
|
var volumeId needle.VolumeId
|
|
for retry := 0; retry < 5; retry++ {
|
|
volumeId, err = uploadTestDataToMaster([]byte(strings.Repeat("multidisk-ec-9593 ", 64)), "127.0.0.1:9334")
|
|
if err == nil {
|
|
break
|
|
}
|
|
time.Sleep(3 * time.Second)
|
|
}
|
|
require.NoError(t, err, "failed to upload test data")
|
|
for i := 0; i < 40; i++ {
|
|
if _, e := uploadTestDataToMaster([]byte(strings.Repeat("filler ", 128)), "127.0.0.1:9334"); e != nil {
|
|
break
|
|
}
|
|
}
|
|
t.Logf("using volume %d", volumeId)
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// Populate every server's disks with volumes so the encode can see and target
|
|
// each physical disk. The master only enumerates disks that already hold a
|
|
// volume or EC shard — an empty disk leaves no trace in the topology (heartbeats
|
|
// aggregate capacity per disk type, not per physical disk). ec.encode therefore
|
|
// spreads a volume's shards only across the disks the master already knows hold
|
|
// data on each node; if a node's data sits on a single disk, all its shards land
|
|
// there and ec.balance cannot redistribute them (it has no within-node
|
|
// cross-disk move). So spreading must be set up before encoding.
|
|
//
|
|
// volume.grow only tops up toward a writable target and stops on the first
|
|
// allocation error, so a single -count grow can create far fewer volumes than
|
|
// asked and leave a node on one disk. Grow repeatedly on the nodes that have not
|
|
// spread yet (the volume server places each new volume on its least-loaded disk)
|
|
// until the master's topology shows every node holding volumes on at least two
|
|
// physical disks. This makes the multi-disk layout — and thus the post-encode
|
|
// disk spread — deterministic instead of racing volume-growth and heartbeat.
|
|
require.Eventually(t, func() bool {
|
|
spread := nodeVolumeDiskCounts(t, commandEnv)
|
|
if len(spread) == 3 && allAtLeast(spread, 2) {
|
|
return true
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
server := fmt.Sprintf("127.0.0.1:809%d", i)
|
|
if spread[server] < 2 {
|
|
captureCommandOutput(t, shell.Commands[findCommandIndex("volume.grow")],
|
|
[]string{"-collection", "test", "-dataNode", server, "-count", "4"}, commandEnv)
|
|
}
|
|
}
|
|
return false
|
|
}, 60*time.Second, 2*time.Second,
|
|
"volumes never spread across >=2 disks on all 3 nodes")
|
|
|
|
locked, unlock := tryLockWithTimeout(t, commandEnv, 15*time.Second)
|
|
require.True(t, locked, "could not acquire shell lock")
|
|
defer unlock()
|
|
|
|
// EC-encode the volume.
|
|
out, err := captureCommandOutput(t, shell.Commands[findCommandIndex("ec.encode")],
|
|
[]string{"-volumeId", fmt.Sprintf("%d", volumeId), "-collection", "test", "-force"}, commandEnv)
|
|
t.Logf("ec.encode output:\n%s", out)
|
|
require.NoError(t, err, "ec.encode failed")
|
|
|
|
// All 14 shards must exist after encoding.
|
|
require.Eventually(t, func() bool {
|
|
return len(collectDistinctShardIDs(testDir, uint32(volumeId))) == erasureShardCount
|
|
}, 30*time.Second, time.Second, "expected all %d EC shards after encode, got %v",
|
|
erasureShardCount, collectDistinctShardIDs(testDir, uint32(volumeId)))
|
|
|
|
beforeBalance := collectDistinctShardIDs(testDir, uint32(volumeId))
|
|
t.Logf("after encode: %d distinct shards on %d disks", len(beforeBalance), disksWithShards(testDir, uint32(volumeId)))
|
|
|
|
// Run ec.balance.
|
|
out, err = captureCommandOutput(t, shell.Commands[findCommandIndex("ec.balance")],
|
|
[]string{"-collection", "test", "-force"}, commandEnv)
|
|
t.Logf("ec.balance output:\n%s", out)
|
|
require.NoError(t, err, "ec.balance failed")
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// The core regression: ec.balance must not lose any shard.
|
|
afterBalance := collectDistinctShardIDs(testDir, uint32(volumeId))
|
|
require.Equal(t, erasureShardCount, len(afterBalance),
|
|
"ec.balance lost shards on multi-disk nodes: had %v, now %v", sortedKeysOf(beforeBalance), sortedKeysOf(afterBalance))
|
|
|
|
// Shards must be spread across more than one physical disk per node overall.
|
|
usedDisks := disksWithShards(testDir, uint32(volumeId))
|
|
assert.Greater(t, usedDisks, 3, "EC shards should span more than one disk per node (got %d disks across 3 nodes)", usedDisks)
|
|
|
|
// cluster.status must count physical disks, not collapse to one per node: it
|
|
// must report at least the disks actually holding this volume's shards (which
|
|
// is already >3 across the 3 nodes). Before the fix it reported 3 (node count).
|
|
require.Eventually(t, func() bool {
|
|
n, ok := clusterStatusDiskCount(t, commandEnv)
|
|
return ok && n >= usedDisks
|
|
}, 30*time.Second, 2*time.Second, "cluster.status never reported the >=%d physical disks holding shards (multi-disk count)", usedDisks)
|
|
|
|
n, _ := clusterStatusDiskCount(t, commandEnv)
|
|
t.Logf("cluster.status reports %d physical disks (>= %d holding this volume's shards)", n, usedDisks)
|
|
}
|
|
|
|
const erasureShardCount = 14 // 10 data + 4 parity
|
|
|
|
// collectDistinctShardIDs returns the set of EC shard ids present for a volume
|
|
// across every disk of every server in the multi-disk test layout.
|
|
func collectDistinctShardIDs(testDir string, volumeId uint32) map[int]bool {
|
|
ids := map[int]bool{}
|
|
for server := 0; server < 3; server++ {
|
|
for disk := 0; disk < 4; disk++ {
|
|
diskDir := filepath.Join(testDir, fmt.Sprintf("server%d_disk%d", server, disk))
|
|
files, err := listECShardFiles(diskDir, volumeId)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, f := range files {
|
|
i := strings.LastIndex(f, ".ec")
|
|
if i < 0 {
|
|
continue
|
|
}
|
|
if n, err := strconv.Atoi(f[i+3:]); err == nil && n >= 0 && n < erasureShardCount {
|
|
ids[n] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// disksWithShards counts how many physical disks hold at least one shard.
|
|
func disksWithShards(testDir string, volumeId uint32) int {
|
|
n := 0
|
|
for _, disks := range countShardsPerDisk(testDir, volumeId) {
|
|
for _, c := range disks {
|
|
if c > 0 {
|
|
n++
|
|
}
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// nodeVolumeDiskCounts returns, per volume server id, how many distinct physical
|
|
// disks hold at least one volume according to the master's topology. The master
|
|
// only enumerates disks that already hold a volume or EC shard (heartbeats
|
|
// aggregate capacity per disk type, not per physical disk), so this reports the
|
|
// disks ec.encode can actually spread a volume's shards across on each node.
|
|
func nodeVolumeDiskCounts(t *testing.T, commandEnv *shell.CommandEnv) map[string]int {
|
|
t.Helper()
|
|
var resp *master_pb.VolumeListResponse
|
|
err := commandEnv.MasterClient.WithClient(false, func(client master_pb.HanzoClient) error {
|
|
var e error
|
|
resp, e = client.VolumeList(context.Background(), &master_pb.VolumeListRequest{})
|
|
return e
|
|
})
|
|
counts := map[string]int{}
|
|
if err != nil || resp.GetTopologyInfo() == nil {
|
|
return counts
|
|
}
|
|
for _, dc := range resp.GetTopologyInfo().GetDataCenterInfos() {
|
|
for _, r := range dc.GetRackInfos() {
|
|
for _, dn := range r.GetDataNodeInfos() {
|
|
disks := map[uint32]bool{}
|
|
for _, di := range dn.GetDiskInfos() {
|
|
for _, vi := range di.GetVolumeInfos() {
|
|
disks[vi.GetDiskId()] = true
|
|
}
|
|
}
|
|
counts[dn.Id] = len(disks)
|
|
}
|
|
}
|
|
}
|
|
return counts
|
|
}
|
|
|
|
func allAtLeast(counts map[string]int, min int) bool {
|
|
for _, c := range counts {
|
|
if c < min {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
var diskCountRe = regexp.MustCompile(`(\d+)\s+disks?`)
|
|
|
|
// clusterStatusDiskCount runs cluster.status and parses the reported disk count.
|
|
func clusterStatusDiskCount(t *testing.T, commandEnv *shell.CommandEnv) (int, bool) {
|
|
t.Helper()
|
|
out, err := captureCommandOutput(t, shell.Commands[findCommandIndex("cluster.status")], []string{}, commandEnv)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
m := diskCountRe.FindStringSubmatch(out)
|
|
if m == nil {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.Atoi(m[1])
|
|
return n, err == nil
|
|
}
|
|
|
|
func sortedKeysOf(m map[int]bool) []int {
|
|
out := make([]int, 0, len(m))
|
|
for k := range m {
|
|
out = append(out, k)
|
|
}
|
|
for i := 1; i < len(out); i++ {
|
|
for j := i; j > 0 && out[j-1] > out[j]; j-- {
|
|
out[j-1], out[j] = out[j], out[j-1]
|
|
}
|
|
}
|
|
return out
|
|
}
|