mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-28 11:56:07 +00:00
The failover harness treated an open volume server port as readiness, but the master only learns of a volume server from its heartbeat. A lone master refuses heartbeats until its bootstrap check elects it, and the servers back off and retry, so registration lands seconds after the ports answer. Tests that started writing in that window assigned against an empty topology, which fails with "no free volumes left" and reaches the mount as ENOSPC.
595 lines
17 KiB
Go
595 lines
17 KiB
Go
//go:build linux || darwin
|
|
|
|
package fuse_failover
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/test/testutil"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// failoverCluster runs 1 master, N volume servers, 1 filer and M FUSE mounts,
|
|
// with the master defaulting to 001 replication so every chunk lands on two
|
|
// distinct volume servers. Individual volume servers can be stopped and started
|
|
// while IO is in flight, which is what the Docker Swarm reports in discussion
|
|
// 10206 exercise: a volume server disappears mid-append and the mounts must
|
|
// keep reading from the surviving replica and keep writing on another volume.
|
|
type failoverCluster struct {
|
|
t testing.TB
|
|
baseDir string
|
|
weedBinary string
|
|
|
|
masterPort int
|
|
masterGrpcPort int
|
|
filerPort int
|
|
filerGrpcPort int
|
|
volumePorts []int
|
|
volumeGrpcPort []int
|
|
volumeDirs []string
|
|
|
|
masterCmd *exec.Cmd
|
|
filerCmd *exec.Cmd
|
|
volumeCmds []*exec.Cmd
|
|
mountCmds []*exec.Cmd
|
|
mountPoints []string
|
|
logFiles []*os.File
|
|
|
|
mu sync.Mutex
|
|
waits map[*exec.Cmd]chan error
|
|
cleanupOnce sync.Once
|
|
}
|
|
|
|
func startFailoverCluster(t testing.TB, numVolumes, numMounts int) *failoverCluster {
|
|
require.GreaterOrEqual(t, numVolumes, 2, "001 replication needs at least 2 volume servers")
|
|
require.GreaterOrEqual(t, numMounts, 1)
|
|
|
|
binary := findWeedBinary()
|
|
if binary == "" {
|
|
t.Skip("weed binary not found; set WEED_BINARY or ensure it is on PATH")
|
|
}
|
|
baseDir, err := os.MkdirTemp("", "seaweedfs_fuse_failover_")
|
|
require.NoError(t, err)
|
|
|
|
c := &failoverCluster{
|
|
t: t,
|
|
baseDir: baseDir,
|
|
weedBinary: binary,
|
|
volumePorts: make([]int, numVolumes),
|
|
volumeGrpcPort: make([]int, numVolumes),
|
|
volumeDirs: make([]string, numVolumes),
|
|
volumeCmds: make([]*exec.Cmd, numVolumes),
|
|
mountCmds: make([]*exec.Cmd, numMounts),
|
|
mountPoints: make([]string, numMounts),
|
|
}
|
|
t.Cleanup(c.Stop)
|
|
|
|
ports, err := testutil.AllocatePorts(4 + 2*numVolumes)
|
|
require.NoError(t, err)
|
|
c.masterPort, c.masterGrpcPort = ports[0], ports[1]
|
|
c.filerPort, c.filerGrpcPort = ports[2], ports[3]
|
|
for i := 0; i < numVolumes; i++ {
|
|
c.volumePorts[i] = ports[4+2*i]
|
|
c.volumeGrpcPort[i] = ports[5+2*i]
|
|
c.volumeDirs[i] = filepath.Join(baseDir, fmt.Sprintf("volume%d", i))
|
|
require.NoError(t, os.MkdirAll(c.volumeDirs[i], 0755))
|
|
}
|
|
|
|
require.NoError(t, c.startMaster())
|
|
require.NoError(t, c.waitForTCP(c.masterCmd, "master",
|
|
fmt.Sprintf("127.0.0.1:%d", c.masterPort), 30*time.Second))
|
|
|
|
for i := 0; i < numVolumes; i++ {
|
|
require.NoError(t, c.StartVolume(i))
|
|
}
|
|
|
|
require.NoError(t, c.startFiler())
|
|
require.NoError(t, c.waitForTCP(c.filerCmd, "filer",
|
|
fmt.Sprintf("127.0.0.1:%d", c.filerGrpcPort), 30*time.Second))
|
|
|
|
for i := 0; i < numMounts; i++ {
|
|
mp := filepath.Join(baseDir, fmt.Sprintf("mount%d", i))
|
|
require.NoError(t, os.MkdirAll(mp, 0755))
|
|
c.mountPoints[i] = mp
|
|
require.NoError(t, c.startMount(i))
|
|
require.NoError(t, c.waitForMount(mp, 30*time.Second),
|
|
"mount %d not ready\n%s", i, c.tailLog(fmt.Sprintf("mount%d", i)))
|
|
}
|
|
require.NoError(t, c.WaitForVolumeServers(60*time.Second))
|
|
return c
|
|
}
|
|
|
|
func (c *failoverCluster) MountDir(i int) string { return c.mountPoints[i] }
|
|
|
|
func (c *failoverCluster) Stop() {
|
|
if c == nil {
|
|
return
|
|
}
|
|
c.cleanupOnce.Do(func() {
|
|
for i := len(c.mountCmds) - 1; i >= 0; i-- {
|
|
c.stopCmd(c.mountCmds[i], syscall.SIGTERM)
|
|
_ = exec.Command("fusermount3", "-u", c.mountPoints[i]).Run()
|
|
_ = exec.Command("fusermount", "-u", c.mountPoints[i]).Run()
|
|
}
|
|
c.stopCmd(c.filerCmd, syscall.SIGTERM)
|
|
for i := len(c.volumeCmds) - 1; i >= 0; i-- {
|
|
c.stopCmd(c.volumeCmds[i], syscall.SIGTERM)
|
|
}
|
|
c.stopCmd(c.masterCmd, syscall.SIGTERM)
|
|
|
|
c.mu.Lock()
|
|
for _, f := range c.logFiles {
|
|
_ = f.Close()
|
|
}
|
|
c.mu.Unlock()
|
|
c.copyLogsForCI()
|
|
if !c.t.Failed() {
|
|
os.RemoveAll(c.baseDir)
|
|
}
|
|
})
|
|
}
|
|
|
|
// KillVolume drops a volume server without letting it deregister, the closest
|
|
// local equivalent of a Swarm task vanishing from the overlay network.
|
|
func (c *failoverCluster) KillVolume(i int) {
|
|
c.stopCmd(c.volumeCmds[i], syscall.SIGKILL)
|
|
c.volumeCmds[i] = nil
|
|
}
|
|
|
|
// StartVolume (re)starts volume server i on its original ports and data dir.
|
|
func (c *failoverCluster) StartVolume(i int) error {
|
|
cmd := exec.Command(c.weedBinary,
|
|
"-logdir="+filepath.Join(c.baseDir, "logs"),
|
|
"volume",
|
|
"-ip=127.0.0.1",
|
|
"-ip.bind=127.0.0.1",
|
|
"-port="+strconv.Itoa(c.volumePorts[i]),
|
|
"-port.grpc="+strconv.Itoa(c.volumeGrpcPort[i]),
|
|
"-master="+c.masterAddress(),
|
|
"-dir="+c.volumeDirs[i],
|
|
"-dataCenter=dc1",
|
|
"-rack=rack1",
|
|
"-max=10",
|
|
)
|
|
c.volumeCmds[i] = cmd
|
|
if err := c.startCmd(cmd, fmt.Sprintf("volume%d", i)); err != nil {
|
|
return err
|
|
}
|
|
return c.waitForTCP(cmd, fmt.Sprintf("volume%d", i),
|
|
fmt.Sprintf("127.0.0.1:%d", c.volumePorts[i]), 30*time.Second)
|
|
}
|
|
|
|
func (c *failoverCluster) startMaster() error {
|
|
c.masterCmd = exec.Command(c.weedBinary,
|
|
"-logdir="+filepath.Join(c.baseDir, "logs"),
|
|
"master",
|
|
"-ip=127.0.0.1",
|
|
"-ip.bind=127.0.0.1",
|
|
"-port="+strconv.Itoa(c.masterPort),
|
|
"-port.grpc="+strconv.Itoa(c.masterGrpcPort),
|
|
"-mdir="+filepath.Join(c.baseDir, "master"),
|
|
"-defaultReplication=001",
|
|
"-volumeSizeLimitMB=64",
|
|
)
|
|
return c.startCmd(c.masterCmd, "master")
|
|
}
|
|
|
|
func (c *failoverCluster) startFiler() error {
|
|
filerDir := filepath.Join(c.baseDir, "filer")
|
|
if err := os.MkdirAll(filerDir, 0755); err != nil {
|
|
return fmt.Errorf("create filer dir: %w", err)
|
|
}
|
|
c.filerCmd = exec.Command(c.weedBinary,
|
|
"-logdir="+filepath.Join(c.baseDir, "logs"),
|
|
"filer",
|
|
"-ip=127.0.0.1",
|
|
"-ip.bind=127.0.0.1",
|
|
"-port="+strconv.Itoa(c.filerPort),
|
|
"-port.grpc="+strconv.Itoa(c.filerGrpcPort),
|
|
"-master="+c.masterAddress(),
|
|
"-defaultReplicaPlacement=001",
|
|
"-defaultStoreDir="+filerDir,
|
|
)
|
|
return c.startCmd(c.filerCmd, "filer")
|
|
}
|
|
|
|
func (c *failoverCluster) startMount(idx int) error {
|
|
cacheDir := filepath.Join(c.baseDir, fmt.Sprintf("cache%d", idx))
|
|
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
|
return fmt.Errorf("create cache dir: %w", err)
|
|
}
|
|
// Chunk-level detail needs -v=4; keep CI at -v=2 so the logs stay small.
|
|
verbosity := os.Getenv("FUSE_FAILOVER_MOUNT_V")
|
|
if verbosity == "" {
|
|
verbosity = "2"
|
|
}
|
|
c.mountCmds[idx] = exec.Command(c.weedBinary,
|
|
"-logdir="+filepath.Join(c.baseDir, "logs"),
|
|
"-v="+verbosity,
|
|
"mount",
|
|
"-filer="+c.filerAddress(),
|
|
"-dir="+c.mountPoints[idx],
|
|
"-filer.path=/",
|
|
"-dirAutoCreate",
|
|
"-allowOthers=false",
|
|
"-replication=001",
|
|
"-cacheDir="+cacheDir,
|
|
)
|
|
return c.startCmd(c.mountCmds[idx], fmt.Sprintf("mount%d", idx))
|
|
}
|
|
|
|
// MasterGet fetches a master HTTP endpoint, e.g. "/dir/status?pretty=y" or
|
|
// "/dir/lookup?volumeId=6", so a failing test can show where the replicas are.
|
|
func (c *failoverCluster) MasterGet(path string) string {
|
|
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d%s", c.masterPort, path))
|
|
if err != nil {
|
|
return fmt.Sprintf("(master %s failed: %v)", path, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Sprintf("(master %s read failed: %v)", path, err)
|
|
}
|
|
return string(body)
|
|
}
|
|
|
|
// FilerGet reads a file back through the filer's own HTTP handler: a view of
|
|
// the chunk list that neither mount's cache can colour.
|
|
func (c *failoverCluster) FilerGet(path string) ([]byte, error) {
|
|
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d%s", c.filerPort, path))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("filer %s: %s", path, resp.Status)
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
// VolumeServerAddress is the address volume server i registers with the master.
|
|
func (c *failoverCluster) VolumeServerAddress(i int) string {
|
|
return fmt.Sprintf("127.0.0.1:%d", c.volumePorts[i])
|
|
}
|
|
|
|
// FileVolumeIds returns the volume ids backing a file, read from the filer's
|
|
// own entry rather than inferred, so a test can tell which servers a given
|
|
// file actually depends on. Manifests are resolved first: a manifest chunk's
|
|
// own fid names the volume holding the manifest, not the data.
|
|
func (c *failoverCluster) FileVolumeIds(path string) ([]uint32, error) {
|
|
body, err := c.FilerGet(path + "?metadata=true&resolveManifest=true")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var entry struct {
|
|
Chunks []struct {
|
|
FileId string `json:"file_id"`
|
|
Fid struct {
|
|
VolumeId uint32 `json:"volume_id"`
|
|
} `json:"fid"`
|
|
} `json:"chunks"`
|
|
}
|
|
if err = json.Unmarshal(body, &entry); err != nil {
|
|
return nil, fmt.Errorf("decode entry %s: %w", path, err)
|
|
}
|
|
seen := make(map[uint32]bool)
|
|
var vids []uint32
|
|
for _, chunk := range entry.Chunks {
|
|
vid := chunk.Fid.VolumeId
|
|
if vid == 0 && chunk.FileId != "" {
|
|
parsed, parseErr := strconv.ParseUint(strings.SplitN(chunk.FileId, ",", 2)[0], 10, 32)
|
|
if parseErr != nil {
|
|
return nil, fmt.Errorf("parse file id %s: %w", chunk.FileId, parseErr)
|
|
}
|
|
vid = uint32(parsed)
|
|
}
|
|
if !seen[vid] {
|
|
seen[vid] = true
|
|
vids = append(vids, vid)
|
|
}
|
|
}
|
|
return vids, nil
|
|
}
|
|
|
|
// VolumeHolders returns the volume server addresses the master currently lists
|
|
// for a volume id.
|
|
func (c *failoverCluster) VolumeHolders(vid uint32) ([]string, error) {
|
|
var lookup struct {
|
|
Locations []struct {
|
|
Url string `json:"url"`
|
|
} `json:"locations"`
|
|
}
|
|
body := c.MasterGet(fmt.Sprintf("/dir/lookup?volumeId=%d", vid))
|
|
if err := json.Unmarshal([]byte(body), &lookup); err != nil {
|
|
return nil, fmt.Errorf("decode lookup for volume %d: %w (%s)", vid, err, body)
|
|
}
|
|
holders := make([]string, 0, len(lookup.Locations))
|
|
for _, loc := range lookup.Locations {
|
|
holders = append(holders, loc.Url)
|
|
}
|
|
return holders, nil
|
|
}
|
|
|
|
// WaitForHolders polls the master until it lists exactly count servers for a
|
|
// volume. The master only drops a dead node after three missed heartbeats, so a
|
|
// test that depends on the cluster's view having caught up has to wait for it.
|
|
func (c *failoverCluster) WaitForHolders(vid uint32, count int, timeout time.Duration) ([]string, error) {
|
|
deadline := time.Now().Add(timeout)
|
|
for {
|
|
holders, err := c.VolumeHolders(vid)
|
|
if err == nil && len(holders) == count {
|
|
return holders, nil
|
|
}
|
|
if time.Now().After(deadline) {
|
|
return holders, fmt.Errorf("volume %d still has %d holders (%v), want %d", vid, len(holders), holders, count)
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// WaitForVolumeServers blocks until the master's topology lists every volume
|
|
// server. A volume server accepts connections well before the master knows it
|
|
// exists: a lone master only elects itself once its bootstrap check expires,
|
|
// and heartbeats sent before that are refused and retried with backoff, so
|
|
// registration lands seconds after the process is up. Until it does the
|
|
// topology has no data node at all, an assign fails with "no free volumes
|
|
// left", and the mount reports that as ENOSPC instead of retrying.
|
|
func (c *failoverCluster) WaitForVolumeServers(timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
for {
|
|
registered := c.registeredVolumeServers()
|
|
var missing []string
|
|
for i := range c.volumePorts {
|
|
if address := c.VolumeServerAddress(i); !registered[address] {
|
|
missing = append(missing, address)
|
|
}
|
|
}
|
|
if len(missing) == 0 {
|
|
return nil
|
|
}
|
|
if time.Now().After(deadline) {
|
|
return fmt.Errorf("volume servers %v not registered with the master within %v\n%s",
|
|
missing, timeout, c.MasterGet("/dir/status?pretty=y"))
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// registeredVolumeServers is the set of volume server addresses the master
|
|
// currently holds in its topology.
|
|
func (c *failoverCluster) registeredVolumeServers() map[string]bool {
|
|
var status struct {
|
|
Topology struct {
|
|
DataCenters []struct {
|
|
Racks []struct {
|
|
DataNodes []struct {
|
|
Url string `json:"Url"`
|
|
} `json:"DataNodes"`
|
|
} `json:"Racks"`
|
|
} `json:"DataCenters"`
|
|
} `json:"Topology"`
|
|
}
|
|
if err := json.Unmarshal([]byte(c.MasterGet("/dir/status")), &status); err != nil {
|
|
return nil
|
|
}
|
|
registered := make(map[string]bool)
|
|
for _, dc := range status.Topology.DataCenters {
|
|
for _, rack := range dc.Racks {
|
|
for _, dn := range rack.DataNodes {
|
|
registered[dn.Url] = true
|
|
}
|
|
}
|
|
}
|
|
return registered
|
|
}
|
|
|
|
// volumeIndexOf maps a server address back to its index, or -1.
|
|
func (c *failoverCluster) volumeIndexOf(address string) int {
|
|
for i := range c.volumePorts {
|
|
if c.VolumeServerAddress(i) == address {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// FileIsOn reports whether any of path's chunks live on the given volume
|
|
// server, i.e. whether taking that server down actually costs this file a
|
|
// replica.
|
|
func (c *failoverCluster) FileIsOn(path, serverAddress string) (bool, error) {
|
|
vids, err := c.FileVolumeIds(path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, vid := range vids {
|
|
holders, holdersErr := c.VolumeHolders(vid)
|
|
if holdersErr != nil {
|
|
return false, holdersErr
|
|
}
|
|
for _, holder := range holders {
|
|
if holder == serverAddress {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (c *failoverCluster) masterAddress() string {
|
|
return string(pb.NewServerAddress("127.0.0.1", c.masterPort, c.masterGrpcPort))
|
|
}
|
|
|
|
func (c *failoverCluster) filerAddress() string {
|
|
return string(pb.NewServerAddress("127.0.0.1", c.filerPort, c.filerGrpcPort))
|
|
}
|
|
|
|
func (c *failoverCluster) startCmd(cmd *exec.Cmd, name string) error {
|
|
logPath := filepath.Join(c.baseDir, "logs")
|
|
if err := os.MkdirAll(logPath, 0755); err != nil {
|
|
return fmt.Errorf("create log dir: %w", err)
|
|
}
|
|
// Append so a restarted volume server keeps the log of its earlier run.
|
|
logFile, err := os.OpenFile(filepath.Join(logPath, name+".log"),
|
|
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.mu.Lock()
|
|
c.logFiles = append(c.logFiles, logFile)
|
|
c.mu.Unlock()
|
|
cmd.Stdout = logFile
|
|
cmd.Stderr = logFile
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
// Reap in the background and publish the result: Signal(0) succeeds for a
|
|
// zombie, so an unreaped child that died at startup would otherwise look
|
|
// alive until the readiness timeout expired.
|
|
ch := make(chan error, 1)
|
|
c.mu.Lock()
|
|
if c.waits == nil {
|
|
c.waits = make(map[*exec.Cmd]chan error)
|
|
}
|
|
c.waits[cmd] = ch
|
|
c.mu.Unlock()
|
|
go func() {
|
|
ch <- cmd.Wait()
|
|
close(ch)
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// waitChan returns the channel carrying cmd's exit, or nil if it was never
|
|
// started through startCmd. It stays readable after the exit is consumed.
|
|
func (c *failoverCluster) waitChan(cmd *exec.Cmd) chan error {
|
|
if cmd == nil {
|
|
return nil
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.waits[cmd]
|
|
}
|
|
|
|
func (c *failoverCluster) tailLog(name string) string {
|
|
data, err := os.ReadFile(filepath.Join(c.baseDir, "logs", name+".log"))
|
|
if err != nil {
|
|
return fmt.Sprintf("(log %s not available: %v)", name, err)
|
|
}
|
|
const maxTail = 8192
|
|
if len(data) > maxTail {
|
|
data = data[len(data)-maxTail:]
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func (c *failoverCluster) copyLogsForCI() {
|
|
// One directory per test: subtests share a log dir name otherwise, and the
|
|
// last one to finish would overwrite the logs of the one that failed.
|
|
ciLogDir := filepath.Join("/tmp/seaweedfs-fuse-failover-logs",
|
|
strings.ReplaceAll(c.t.Name(), "/", "_"))
|
|
os.MkdirAll(ciLogDir, 0755)
|
|
entries, err := os.ReadDir(filepath.Join(c.baseDir, "logs"))
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, e := range entries {
|
|
data, err := os.ReadFile(filepath.Join(c.baseDir, "logs", e.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
os.WriteFile(filepath.Join(ciLogDir, e.Name()), data, 0644)
|
|
}
|
|
}
|
|
|
|
func (c *failoverCluster) waitForTCP(cmd *exec.Cmd, name, addr string, timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
conn, err := net.DialTimeout("tcp", addr, time.Second)
|
|
if err == nil {
|
|
conn.Close()
|
|
return nil
|
|
}
|
|
if ch := c.waitChan(cmd); ch != nil {
|
|
select {
|
|
case waitErr := <-ch:
|
|
return fmt.Errorf("%s exited before listening on %s: %v\n%s",
|
|
name, addr, waitErr, c.tailLog(name))
|
|
default:
|
|
}
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
return fmt.Errorf("service at %s not ready within timeout\n%s", addr, c.tailLog(name))
|
|
}
|
|
|
|
func (c *failoverCluster) waitForMount(mountPoint string, timeout time.Duration) error {
|
|
parentDir := filepath.Dir(mountPoint)
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
parentStat, err := os.Stat(parentDir)
|
|
if err != nil {
|
|
time.Sleep(200 * time.Millisecond)
|
|
continue
|
|
}
|
|
mountStat, err := os.Stat(mountPoint)
|
|
if err != nil {
|
|
time.Sleep(200 * time.Millisecond)
|
|
continue
|
|
}
|
|
if parentStat.Sys().(*syscall.Stat_t).Dev != mountStat.Sys().(*syscall.Stat_t).Dev {
|
|
return nil
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
return fmt.Errorf("mount point %s not ready within timeout (FUSE not detected)", mountPoint)
|
|
}
|
|
|
|
func findWeedBinary() string {
|
|
if env := os.Getenv("WEED_BINARY"); env != "" {
|
|
if _, err := os.Stat(env); err == nil {
|
|
return env
|
|
}
|
|
}
|
|
if p, err := exec.LookPath("weed"); err == nil {
|
|
return p
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// stopCmd signals cmd and waits for the reaper goroutine started by startCmd to
|
|
// report its exit, escalating to SIGKILL if it does not go quietly.
|
|
func (c *failoverCluster) stopCmd(cmd *exec.Cmd, sig syscall.Signal) {
|
|
if cmd == nil || cmd.Process == nil {
|
|
return
|
|
}
|
|
_ = cmd.Process.Signal(sig)
|
|
done := c.waitChan(cmd)
|
|
if done == nil {
|
|
return
|
|
}
|
|
select {
|
|
case <-done:
|
|
case <-time.After(10 * time.Second):
|
|
_ = cmd.Process.Signal(syscall.SIGKILL)
|
|
<-done
|
|
}
|
|
}
|