mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
fix(mini): shut down admin/s3/webdav/filer before volume/master on Ctrl+C (#9112)
* fix(mini): shut down admin/s3/webdav/filer before volume/master on Ctrl+C Interrupts fired grace hooks in registration order, so master (started first) shut down before its clients, producing heartbeat-canceled errors and masterClient reconnection noise during weed mini shutdown. Admin/s3/ webdav had no interrupt hooks at all and were killed at os.Exit. - grace: execute interrupt hooks in LIFO (defer-style) order so later- started services tear down first. - filer: consolidate the three separate interrupt hooks (gRPC / HTTP / DB) into one that runs in order, so filer shutdown stays correct independent of FIFO/LIFO semantics. - mini: add MiniClientsShutdownCtx (separate from test-facing MiniClusterCtx) plus an OnMiniClientsShutdown helper. Admin, S3, WebDAV and the maintenance worker observe it; runMini registers a cancel hook after startup so under LIFO it fires first and waits up to 10s on a WaitGroup for those services to drain before filer, volume, and master shut down. Resulting order on Ctrl+C: admin/s3/webdav/worker -> filer (gRPC -> HTTP -> DB) -> volume -> master. * refactor(mini): group mini-client shutdown into one state struct The first pass spread the shutdown plumbing across three globals (MiniClientsShutdownCtx, miniClientsWg, cancelMiniClients) and two ctx-derivation sites (OnMiniClientsShutdown and startMiniAdminWithWorker). Group into a private miniClientsState (ctx/cancel/wg) rebuilt per runMini invocation, and chain its ctx from MiniClusterCtx so clients only observe one signal. Tests that cancel MiniClusterCtx still trigger client shutdown via parent-child propagation. - resetMiniClients() installs fresh state at the top of runMini, so in-process test reruns don't inherit stale ctx/wg. - onMiniClientsShutdown(fn) replaces the exported OnMiniClientsShutdown and only observes one ctx. - trackMiniClient() replaces the manual wg.Add/Done dance for the admin goroutine. - miniClientsCtx() gives the admin startup a ctx without re-deriving. - triggerMiniClientsShutdown(timeout) is the interrupt hook body. No behaviour change; existing tests pass. * refactor: generalize shutdown ctx as an option, not a mini-specific helper Several service files (s3, webdav, filer, master, volume) observed the mini-specific MiniClusterCtx or called onMiniClientsShutdown directly. That leaked mini orchestration into code that also runs under weed s3, weed webdav, weed filer, weed master, and weed volume standalone. Replace with a generic `shutdownCtx context.Context` field on each service's Options struct. When non-nil, the server watches it and shuts down gracefully; when nil (standalone), the shutdown path is a no-op. Mini wires the contexts up from a single place (runMini): - miniMasterOptions/miniOptions.v/miniFilerOptions.shutdownCtx = MiniClusterCtx (drives test-triggered teardown) - miniS3Options/miniWebDavOptions.shutdownCtx = miniClientsCtx() (drives Ctrl+C teardown before filer/volume/master) All knowledge of MiniClusterCtx now lives in mini.go. * fix(mini): stop worker before clients ctx so admin shutdown isn't blocked Symptom on Ctrl+C of a clean weed mini: mini's Shutting down admin/s3/ webdav hook sat for 10s then logged "timed out". Admin had started its shutdown but was blocked inside StopWorkerGrpcServer's GracefulStop, waiting for the still-connected worker stream. That in turn left filer clients connected and cascaded into filer's own 10s gRPC graceful-stop timeout. Two causes, both fixed: 1. worker.Stop() deadlocked on clean shutdown. It sent ActionStop (which makes managerLoop `break out` and exit), then called getTaskLoad() which sends to the same unbuffered cmd channel — no receiver, hangs forever. Reorder Stop() to snapshot the admin client and drain tasks BEFORE sending ActionStop, and call Disconnect() via the local snapshot afterwards. 2. Worker's taskRequestLoop raced with Disconnect(): RequestTask reads from c.incoming, which Disconnect closes, yielding a nil response and a panic on response.Message. Handle the closed channel explicitly. 3. Mini now has a preCancel phase (beforeMiniClientsShutdown) that runs synchronously BEFORE the clients ctx is cancelled. Register worker shutdown there so admin's worker-gRPC GracefulStop finds the worker already disconnected and returns immediately, instead of waiting on a stream that is about to close anyway. Observed shutdown of a clean mini: admin/s3/webdav down in <10ms; full process exit in ~11s (the remaining 10s is a pre-existing filer gRPC graceful-stop timeout, not cascaded from the clients tier). * feat(mini): cap filer gRPC graceful stop at 1s under weed mini Full weed mini shutdown was ~11s on a clean exit, dominated by the filer's default 10s gRPC GracefulStop timeout while background SubscribeLocalMetadata streams drained. Expose the timeout as a FilerOptions.gracefulStopTimeout field (default 10s for standalone weed filer) and set it to 1s in mini. Clean weed mini shutdown now takes ~2s.
This commit is contained in:
+32
-15
@@ -84,6 +84,13 @@ type FilerOptions struct {
|
|||||||
tusBasePath *string
|
tusBasePath *string
|
||||||
certProvider certprovider.Provider
|
certProvider certprovider.Provider
|
||||||
s3ConfigFile *string // optional path to static S3 identity config
|
s3ConfigFile *string // optional path to static S3 identity config
|
||||||
|
// shutdownCtx, when non-nil, tells startFiler to gracefully shut down its
|
||||||
|
// HTTP/gRPC servers once the ctx is cancelled. Used by integration tests
|
||||||
|
// and by weed mini; nil for standalone weed filer.
|
||||||
|
shutdownCtx context.Context
|
||||||
|
// gracefulStopTimeout caps how long startFiler waits for gRPC graceful
|
||||||
|
// stop before forcing the server to stop. Zero means the default of 10s.
|
||||||
|
gracefulStopTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -446,8 +453,12 @@ func (fo *FilerOptions) startFiler() {
|
|||||||
go grpcS.Serve(grpcL)
|
go grpcS.Serve(grpcL)
|
||||||
pb.ServeGrpcOnLocalSocket(grpcS, grpcPort)
|
pb.ServeGrpcOnLocalSocket(grpcS, grpcPort)
|
||||||
|
|
||||||
// Register graceful shutdown for gRPC server to wait for active RPCs
|
// Helper to gracefully stop the gRPC server, waiting for active RPCs.
|
||||||
grace.OnInterrupt(func() {
|
gracefulTimeout := fo.gracefulStopTimeout
|
||||||
|
if gracefulTimeout <= 0 {
|
||||||
|
gracefulTimeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
stopGrpcServer := func() {
|
||||||
glog.V(0).Infof("Gracefully stopping gRPC server")
|
glog.V(0).Infof("Gracefully stopping gRPC server")
|
||||||
stopped := make(chan struct{})
|
stopped := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
@@ -457,11 +468,11 @@ func (fo *FilerOptions) startFiler() {
|
|||||||
select {
|
select {
|
||||||
case <-stopped:
|
case <-stopped:
|
||||||
glog.V(0).Infof("gRPC server stopped gracefully")
|
glog.V(0).Infof("gRPC server stopped gracefully")
|
||||||
case <-time.After(10 * time.Second):
|
case <-time.After(gracefulTimeout):
|
||||||
glog.V(0).Infof("gRPC server graceful stop timed out, forcing stop")
|
glog.V(0).Infof("gRPC server graceful stop timed out after %s, forcing stop", gracefulTimeout)
|
||||||
grpcS.Stop()
|
grpcS.Stop()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
var socketServer *http.Server
|
var socketServer *http.Server
|
||||||
if runtime.GOOS != "windows" {
|
if runtime.GOOS != "windows" {
|
||||||
@@ -528,8 +539,12 @@ func (fo *FilerOptions) startFiler() {
|
|||||||
}
|
}
|
||||||
httpS := newHttpServer(defaultMux, tlsConfig)
|
httpS := newHttpServer(defaultMux, tlsConfig)
|
||||||
|
|
||||||
// Register shutdown hooks: stop all HTTP servers, then close filer database
|
// Register a single shutdown hook that runs the steps in the correct order:
|
||||||
|
// stop accepting new gRPC/HTTP requests, then close the filer database.
|
||||||
|
// Combining them into one hook keeps ordering intact regardless of how
|
||||||
|
// grace fires interrupt hooks (FIFO vs LIFO).
|
||||||
grace.OnInterrupt(func() {
|
grace.OnInterrupt(func() {
|
||||||
|
stopGrpcServer()
|
||||||
glog.V(0).Infof("Gracefully stopping all HTTP servers")
|
glog.V(0).Infof("Gracefully stopping all HTTP servers")
|
||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -542,13 +557,12 @@ func (fo *FilerOptions) startFiler() {
|
|||||||
if err := httpS.Shutdown(shutdownCtx); err != nil {
|
if err := httpS.Shutdown(shutdownCtx); err != nil {
|
||||||
glog.Warningf("HTTPS server shutdown: %v", err)
|
glog.Warningf("HTTPS server shutdown: %v", err)
|
||||||
}
|
}
|
||||||
|
shutdownFiler()
|
||||||
})
|
})
|
||||||
grace.OnInterrupt(shutdownFiler)
|
|
||||||
|
|
||||||
if MiniClusterCtx != nil {
|
if fo.shutdownCtx != nil {
|
||||||
ctx := MiniClusterCtx
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-fo.shutdownCtx.Done()
|
||||||
httpS.Shutdown(context.Background())
|
httpS.Shutdown(context.Background())
|
||||||
grpcS.Stop()
|
grpcS.Stop()
|
||||||
}()
|
}()
|
||||||
@@ -570,8 +584,12 @@ func (fo *FilerOptions) startFiler() {
|
|||||||
}
|
}
|
||||||
httpS := newHttpServer(defaultMux, nil)
|
httpS := newHttpServer(defaultMux, nil)
|
||||||
|
|
||||||
// Register shutdown hooks: stop all HTTP servers, then close filer database
|
// Register a single shutdown hook that runs the steps in the correct order:
|
||||||
|
// stop accepting new gRPC/HTTP requests, then close the filer database.
|
||||||
|
// Combining them into one hook keeps ordering intact regardless of how
|
||||||
|
// grace fires interrupt hooks (FIFO vs LIFO).
|
||||||
grace.OnInterrupt(func() {
|
grace.OnInterrupt(func() {
|
||||||
|
stopGrpcServer()
|
||||||
glog.V(0).Infof("Gracefully stopping all HTTP servers")
|
glog.V(0).Infof("Gracefully stopping all HTTP servers")
|
||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -584,13 +602,12 @@ func (fo *FilerOptions) startFiler() {
|
|||||||
if err := httpS.Shutdown(shutdownCtx); err != nil {
|
if err := httpS.Shutdown(shutdownCtx); err != nil {
|
||||||
glog.Warningf("HTTP server shutdown: %v", err)
|
glog.Warningf("HTTP server shutdown: %v", err)
|
||||||
}
|
}
|
||||||
|
shutdownFiler()
|
||||||
})
|
})
|
||||||
grace.OnInterrupt(shutdownFiler)
|
|
||||||
|
|
||||||
if MiniClusterCtx != nil {
|
if fo.shutdownCtx != nil {
|
||||||
ctx := MiniClusterCtx
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-fo.shutdownCtx.Done()
|
||||||
httpS.Shutdown(context.Background())
|
httpS.Shutdown(context.Background())
|
||||||
grpcS.Stop()
|
grpcS.Stop()
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ type MasterOptions struct {
|
|||||||
telemetryEnabled *bool
|
telemetryEnabled *bool
|
||||||
debug *bool
|
debug *bool
|
||||||
debugPort *int
|
debugPort *int
|
||||||
|
// shutdownCtx, when non-nil, tells startMaster to shut down once the ctx
|
||||||
|
// is cancelled. Used by integration tests and by weed mini; nil for
|
||||||
|
// standalone weed master.
|
||||||
|
shutdownCtx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -336,9 +340,8 @@ func startMaster(masterOption MasterOptions, masterWhiteList []string) {
|
|||||||
ms.Topo.HashicorpRaft.LeadershipTransfer()
|
ms.Topo.HashicorpRaft.LeadershipTransfer()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
ctx := MiniClusterCtx
|
if masterOption.shutdownCtx != nil {
|
||||||
if ctx != nil {
|
<-masterOption.shutdownCtx.Done()
|
||||||
<-ctx.Done()
|
|
||||||
ms.Shutdown()
|
ms.Shutdown()
|
||||||
grpcS.Stop()
|
grpcS.Stop()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+171
-21
@@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||||
@@ -66,6 +67,123 @@ var (
|
|||||||
MiniClusterCtx context.Context
|
MiniClusterCtx context.Context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// miniClientsState orchestrates graceful shutdown of admin/s3/webdav/worker on
|
||||||
|
// weed mini Ctrl+C, BEFORE filer/volume/master tear down. It is rebuilt on
|
||||||
|
// each runMini invocation so in-process test reruns see fresh state.
|
||||||
|
//
|
||||||
|
// The ctx chains from MiniClusterCtx so cancelling MiniClusterCtx (how tests
|
||||||
|
// tear down the cluster) also triggers the client-shutdown path.
|
||||||
|
//
|
||||||
|
// Shutdown has two phases:
|
||||||
|
// 1. preCancelFns run synchronously in registration order (worker disconnect,
|
||||||
|
// etc.) so downstream servers don't block on handlers that are about to
|
||||||
|
// close anyway.
|
||||||
|
// 2. ctx is cancelled and the shutdown hook waits on wg for admin/s3/webdav
|
||||||
|
// goroutines (registered via onMiniClientsShutdown / trackMiniClient) to
|
||||||
|
// drain.
|
||||||
|
type miniClientsState struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
wg sync.WaitGroup
|
||||||
|
preCancelMu sync.Mutex
|
||||||
|
preCancelFns []func()
|
||||||
|
}
|
||||||
|
|
||||||
|
var miniClients *miniClientsState
|
||||||
|
|
||||||
|
// resetMiniClients installs a fresh client-shutdown state chained from
|
||||||
|
// MiniClusterCtx. Called once at the top of runMini; any goroutines from a
|
||||||
|
// prior invocation keep their old state via closure and are unaffected.
|
||||||
|
func resetMiniClients() {
|
||||||
|
parent := context.Background()
|
||||||
|
if MiniClusterCtx != nil {
|
||||||
|
parent = MiniClusterCtx
|
||||||
|
}
|
||||||
|
s := &miniClientsState{}
|
||||||
|
s.ctx, s.cancel = context.WithCancel(parent)
|
||||||
|
miniClients = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// onMiniClientsShutdown runs fn when mini shutdown is triggered, and tracks
|
||||||
|
// it so the interrupt hook can wait for it to drain. No-op outside mini.
|
||||||
|
func onMiniClientsShutdown(fn func()) {
|
||||||
|
s := miniClients
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer s.wg.Done()
|
||||||
|
<-s.ctx.Done()
|
||||||
|
fn()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// trackMiniClient registers an externally-managed goroutine (one that
|
||||||
|
// observes miniClientsCtx() itself) so the interrupt hook waits for it.
|
||||||
|
// The caller invokes the returned done func when the goroutine exits.
|
||||||
|
func trackMiniClient() (done func()) {
|
||||||
|
s := miniClients
|
||||||
|
if s == nil {
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
s.wg.Add(1)
|
||||||
|
return s.wg.Done
|
||||||
|
}
|
||||||
|
|
||||||
|
// beforeMiniClientsShutdown registers fn to run synchronously BEFORE the
|
||||||
|
// clients ctx is cancelled. Use for cleanup that must complete before
|
||||||
|
// downstream servers (e.g., the admin worker-gRPC) start waiting on clients.
|
||||||
|
func beforeMiniClientsShutdown(fn func()) {
|
||||||
|
s := miniClients
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.preCancelMu.Lock()
|
||||||
|
defer s.preCancelMu.Unlock()
|
||||||
|
s.preCancelFns = append(s.preCancelFns, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// miniClientsCtx returns the shutdown context for mini clients, or
|
||||||
|
// context.Background() if not running inside weed mini.
|
||||||
|
func miniClientsCtx() context.Context {
|
||||||
|
s := miniClients
|
||||||
|
if s == nil {
|
||||||
|
return context.Background()
|
||||||
|
}
|
||||||
|
return s.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// triggerMiniClientsShutdown runs preCancel fns synchronously, cancels the
|
||||||
|
// clients ctx, and waits up to timeout for tracked goroutines to finish.
|
||||||
|
// Called from the OnInterrupt hook.
|
||||||
|
func triggerMiniClientsShutdown(timeout time.Duration) {
|
||||||
|
s := miniClients
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
glog.V(0).Infof("Shutting down admin/s3/webdav ...")
|
||||||
|
s.preCancelMu.Lock()
|
||||||
|
fns := s.preCancelFns
|
||||||
|
s.preCancelFns = nil
|
||||||
|
s.preCancelMu.Unlock()
|
||||||
|
for _, fn := range fns {
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
s.cancel()
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
s.wg.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
glog.V(0).Infof("admin/s3/webdav shut down")
|
||||||
|
case <-time.After(timeout):
|
||||||
|
glog.V(0).Infof("timed out waiting for admin/s3/webdav to shut down")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cmdMini.Run = runMini // break init cycle
|
cmdMini.Run = runMini // break init cycle
|
||||||
}
|
}
|
||||||
@@ -869,6 +987,21 @@ func runMini(cmd *Command, args []string) bool {
|
|||||||
|
|
||||||
miniWhiteList := util.StringSplit(*miniWhiteListOption, ",")
|
miniWhiteList := util.StringSplit(*miniWhiteListOption, ",")
|
||||||
|
|
||||||
|
// Install a fresh clients-shutdown context (chained from MiniClusterCtx)
|
||||||
|
// before any service starts.
|
||||||
|
resetMiniClients()
|
||||||
|
|
||||||
|
// Master/volume/filer observe MiniClusterCtx so tests that cancel it
|
||||||
|
// tear those services down too. On Ctrl+C they rely on their own
|
||||||
|
// OnInterrupt hooks (see grace's LIFO ordering).
|
||||||
|
miniMasterOptions.shutdownCtx = MiniClusterCtx
|
||||||
|
miniOptions.v.shutdownCtx = MiniClusterCtx
|
||||||
|
miniFilerOptions.shutdownCtx = MiniClusterCtx
|
||||||
|
// Mini is a small/dev setup with short-lived RPCs; cap the filer's
|
||||||
|
// gRPC graceful-stop at 1s so Ctrl+C returns quickly instead of sitting
|
||||||
|
// on the default 10s waiting for background subscription streams.
|
||||||
|
miniFilerOptions.gracefulStopTimeout = 1 * time.Second
|
||||||
|
|
||||||
// Start all services with proper dependency coordination
|
// Start all services with proper dependency coordination
|
||||||
// This channel will be closed when all services are fully ready
|
// This channel will be closed when all services are fully ready
|
||||||
allServicesReady := make(chan struct{})
|
allServicesReady := make(chan struct{})
|
||||||
@@ -877,6 +1010,13 @@ func runMini(cmd *Command, args []string) bool {
|
|||||||
// Wait for all services to be fully running before printing welcome message
|
// Wait for all services to be fully running before printing welcome message
|
||||||
<-allServicesReady
|
<-allServicesReady
|
||||||
|
|
||||||
|
// Register the clients-shutdown interrupt hook AFTER all services have
|
||||||
|
// registered theirs. Under grace's LIFO firing, this hook runs FIRST on
|
||||||
|
// Ctrl+C so admin/s3/webdav drain before filer/volume/master tear down.
|
||||||
|
grace.OnInterrupt(func() {
|
||||||
|
triggerMiniClientsShutdown(10 * time.Second)
|
||||||
|
})
|
||||||
|
|
||||||
// Print welcome message after all services are running
|
// Print welcome message after all services are running
|
||||||
printWelcomeMessage()
|
printWelcomeMessage()
|
||||||
|
|
||||||
@@ -921,17 +1061,27 @@ func startMiniServices(miniWhiteList []string, allServicesReady chan struct{}) {
|
|||||||
// Wait for filer to be ready
|
// Wait for filer to be ready
|
||||||
waitForServiceReady("Filer", *miniFilerOptions.port, bindIp)
|
waitForServiceReady("Filer", *miniFilerOptions.port, bindIp)
|
||||||
|
|
||||||
// Start S3 and WebDAV in parallel (both depend on filer)
|
// Start S3 and WebDAV in parallel (both depend on filer). Each observes
|
||||||
|
// miniClientsCtx so it shuts down first on Ctrl+C, tracked via
|
||||||
|
// trackMiniClient so runMini's interrupt hook can wait for them.
|
||||||
if *miniEnableS3 {
|
if *miniEnableS3 {
|
||||||
go startMiniService("S3", func() {
|
miniS3Options.shutdownCtx = miniClientsCtx()
|
||||||
startS3Service()
|
done := trackMiniClient()
|
||||||
}, *miniS3Options.port)
|
go func() {
|
||||||
|
defer done()
|
||||||
|
startMiniService("S3", startS3Service, *miniS3Options.port)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if *miniEnableWebDAV {
|
if *miniEnableWebDAV {
|
||||||
go startMiniService("WebDAV", func() {
|
miniWebDavOptions.shutdownCtx = miniClientsCtx()
|
||||||
miniWebDavOptions.startWebDav()
|
done := trackMiniClient()
|
||||||
}, *miniWebDavOptions.port)
|
go func() {
|
||||||
|
defer done()
|
||||||
|
startMiniService("WebDAV", func() {
|
||||||
|
miniWebDavOptions.startWebDav()
|
||||||
|
}, *miniWebDavOptions.port)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for services to be ready
|
// Wait for services to be ready
|
||||||
@@ -999,12 +1149,8 @@ func startS3Service() {
|
|||||||
func startMiniAdminWithWorker(allServicesReady chan struct{}) {
|
func startMiniAdminWithWorker(allServicesReady chan struct{}) {
|
||||||
defer close(allServicesReady) // Ensure channel is always closed on all paths
|
defer close(allServicesReady) // Ensure channel is always closed on all paths
|
||||||
|
|
||||||
var ctx context.Context
|
// Admin shuts down when mini clients shutdown is triggered.
|
||||||
if MiniClusterCtx != nil {
|
ctx := miniClientsCtx()
|
||||||
ctx = MiniClusterCtx
|
|
||||||
} else {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine bind IP for health checks
|
// Determine bind IP for health checks
|
||||||
bindIp := getBindIp()
|
bindIp := getBindIp()
|
||||||
@@ -1044,8 +1190,12 @@ func startMiniAdminWithWorker(allServicesReady chan struct{}) {
|
|||||||
*miniAdminOptions.dataDir = filepath.Join(*miniDataFolders, "admin")
|
*miniAdminOptions.dataDir = filepath.Join(*miniDataFolders, "admin")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start admin server in background
|
// Start admin server in background. trackMiniClient lets the Ctrl+C
|
||||||
|
// handler wait for startAdminServer's graceful shutdown before filer/
|
||||||
|
// volume/master tear down.
|
||||||
|
done := trackMiniClient()
|
||||||
go func() {
|
go func() {
|
||||||
|
defer done()
|
||||||
var icebergPort int
|
var icebergPort int
|
||||||
if miniS3Options.portIceberg != nil {
|
if miniS3Options.portIceberg != nil {
|
||||||
icebergPort = *miniS3Options.portIceberg
|
icebergPort = *miniS3Options.portIceberg
|
||||||
@@ -1189,13 +1339,13 @@ func startMiniWorker(workerDir string) {
|
|||||||
|
|
||||||
// Metrics server is already started in the main init function above, so no need to start it again here
|
// Metrics server is already started in the main init function above, so no need to start it again here
|
||||||
|
|
||||||
// Start the worker
|
// Stop the worker BEFORE the clients ctx is cancelled. Otherwise admin's
|
||||||
if MiniClusterCtx != nil {
|
// internal worker gRPC GracefulStop (called during admin.Shutdown) would
|
||||||
go func() {
|
// wait for the worker stream to close, blocking the whole mini shutdown
|
||||||
<-MiniClusterCtx.Done()
|
// by ~10s and cascading into filer's own gRPC graceful stop timeout.
|
||||||
workerInstance.Stop()
|
beforeMiniClientsShutdown(func() {
|
||||||
}()
|
workerInstance.Stop()
|
||||||
}
|
})
|
||||||
err = workerInstance.Start()
|
err = workerInstance.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Fatalf("Failed to start worker: %v", err)
|
glog.Fatalf("Failed to start worker: %v", err)
|
||||||
|
|||||||
+11
-7
@@ -74,6 +74,11 @@ type S3Options struct {
|
|||||||
externalUrl *string
|
externalUrl *string
|
||||||
defaultFileMode *string
|
defaultFileMode *string
|
||||||
cacheSizeMB *int64
|
cacheSizeMB *int64
|
||||||
|
// shutdownCtx, when non-nil, tells startS3Server/startIcebergServer to
|
||||||
|
// gracefully shut down their HTTP/gRPC servers once the ctx is cancelled.
|
||||||
|
// Used by weed mini to orchestrate an ordered shutdown; nil for standalone
|
||||||
|
// weed s3.
|
||||||
|
shutdownCtx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -453,10 +458,9 @@ func (s3opt *S3Options) startS3Server() bool {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
httpS := newHttpServer(router, tlsConfig)
|
httpS := newHttpServer(router, tlsConfig)
|
||||||
if MiniClusterCtx != nil {
|
if s3opt.shutdownCtx != nil {
|
||||||
ctx := MiniClusterCtx
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-s3opt.shutdownCtx.Done()
|
||||||
httpS.Shutdown(context.Background())
|
httpS.Shutdown(context.Background())
|
||||||
grpcS.Stop()
|
grpcS.Stop()
|
||||||
}()
|
}()
|
||||||
@@ -495,9 +499,9 @@ func (s3opt *S3Options) startS3Server() bool {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
httpS := newHttpServer(router, nil)
|
httpS := newHttpServer(router, nil)
|
||||||
if MiniClusterCtx != nil {
|
if s3opt.shutdownCtx != nil {
|
||||||
go func() {
|
go func() {
|
||||||
<-MiniClusterCtx.Done()
|
<-s3opt.shutdownCtx.Done()
|
||||||
httpS.Shutdown(context.Background())
|
httpS.Shutdown(context.Background())
|
||||||
grpcS.Stop()
|
grpcS.Stop()
|
||||||
}()
|
}()
|
||||||
@@ -531,9 +535,9 @@ func (s3opt *S3Options) startIcebergServer(s3ApiServer *s3api.S3ApiServer) {
|
|||||||
glog.V(0).Infof("Start Iceberg REST Catalog Server at http://%s", listenAddress)
|
glog.V(0).Infof("Start Iceberg REST Catalog Server at http://%s", listenAddress)
|
||||||
|
|
||||||
httpS := newHttpServer(icebergRouter, nil)
|
httpS := newHttpServer(icebergRouter, nil)
|
||||||
if MiniClusterCtx != nil {
|
if s3opt.shutdownCtx != nil {
|
||||||
go func() {
|
go func() {
|
||||||
<-MiniClusterCtx.Done()
|
<-s3opt.shutdownCtx.Done()
|
||||||
httpS.Shutdown(context.Background())
|
httpS.Shutdown(context.Background())
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
httppprof "net/http/pprof"
|
httppprof "net/http/pprof"
|
||||||
@@ -75,6 +76,10 @@ type VolumeServerOptions struct {
|
|||||||
ldbTimeout *int64
|
ldbTimeout *int64
|
||||||
debug *bool
|
debug *bool
|
||||||
debugPort *int
|
debugPort *int
|
||||||
|
// shutdownCtx, when non-nil, tells startVolumeServer to shut down once the
|
||||||
|
// ctx is cancelled. Used by integration tests and by weed mini; nil for
|
||||||
|
// standalone weed volume.
|
||||||
|
shutdownCtx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -327,11 +332,10 @@ func (v VolumeServerOptions) startVolumeServer(volumeFolders, maxVolumeCounts, v
|
|||||||
stopChan <- true
|
stopChan <- true
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx := MiniClusterCtx
|
if v.shutdownCtx != nil {
|
||||||
if ctx != nil {
|
|
||||||
select {
|
select {
|
||||||
case <-stopChan:
|
case <-stopChan:
|
||||||
case <-ctx.Done():
|
case <-v.shutdownCtx.Done():
|
||||||
shutdown(publicHttpDown, clusterHttpServer, grpcS, volumeServer)
|
shutdown(publicHttpDown, clusterHttpServer, grpcS, volumeServer)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ type WebDavOption struct {
|
|||||||
cacheDir *string
|
cacheDir *string
|
||||||
cacheSizeMB *int64
|
cacheSizeMB *int64
|
||||||
maxMB *int
|
maxMB *int
|
||||||
|
// shutdownCtx, when non-nil, tells startWebDav to gracefully shut down the
|
||||||
|
// HTTP server once the ctx is cancelled. Used by weed mini; nil for
|
||||||
|
// standalone weed webdav.
|
||||||
|
shutdownCtx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -137,9 +141,9 @@ func (wo *WebDavOption) startWebDav() bool {
|
|||||||
glog.Fatalf("WebDav Server listener on %s error: %v", listenAddress, err)
|
glog.Fatalf("WebDav Server listener on %s error: %v", listenAddress, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if MiniClusterCtx != nil {
|
if wo.shutdownCtx != nil {
|
||||||
go func() {
|
go func() {
|
||||||
<-MiniClusterCtx.Done()
|
<-wo.shutdownCtx.Done()
|
||||||
httpS.Shutdown(context.Background())
|
httpS.Shutdown(context.Background())
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,10 @@ func init() {
|
|||||||
reloadHookLock.RUnlock()
|
reloadHookLock.RUnlock()
|
||||||
} else {
|
} else {
|
||||||
interruptHookLock.RLock()
|
interruptHookLock.RLock()
|
||||||
for _, hook := range interruptHooks {
|
// Execute hooks in reverse registration order (LIFO/defer-style) so
|
||||||
|
// later-started services shut down before the services they depend on.
|
||||||
|
for i := len(interruptHooks) - 1; i >= 0; i-- {
|
||||||
|
hook := interruptHooks[i]
|
||||||
glog.V(4).Infof("exec interrupt hook func name:%s", GetFunctionName(hook))
|
glog.V(4).Infof("exec interrupt hook func name:%s", GetFunctionName(hook))
|
||||||
hook()
|
hook()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -838,7 +838,11 @@ func (c *GrpcAdminClient) RequestTask(workerID string, capabilities []types.Task
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case response := <-c.incoming:
|
case response, ok := <-c.incoming:
|
||||||
|
if !ok || response == nil {
|
||||||
|
// incoming was closed (e.g. during Disconnect).
|
||||||
|
return nil, fmt.Errorf("incoming channel closed")
|
||||||
|
}
|
||||||
glog.V(4).Infof("RESPONSE RECEIVED: Worker %s received response from admin server: %T", workerID, response.Message)
|
glog.V(4).Infof("RESPONSE RECEIVED: Worker %s received response from admin server: %T", workerID, response.Message)
|
||||||
if taskAssign := response.GetTaskAssignment(); taskAssign != nil {
|
if taskAssign := response.GetTaskAssignment(); taskAssign != nil {
|
||||||
// Validate TaskId is not empty before processing
|
// Validate TaskId is not empty before processing
|
||||||
|
|||||||
+19
-11
@@ -465,16 +465,14 @@ func (w *Worker) handleStart(cmd workerCommand) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *Worker) Stop() error {
|
func (w *Worker) Stop() error {
|
||||||
resp := make(chan error)
|
// Snapshot the admin client BEFORE ActionStop terminates the manager loop.
|
||||||
w.cmds <- workerCommand{
|
// Once the loop exits, any further w.cmds send (getTaskLoad, getAdmin, etc.)
|
||||||
action: ActionStop,
|
// would deadlock because no one reads w.cmds anymore.
|
||||||
resp: resp,
|
adminClient := w.getAdmin()
|
||||||
}
|
|
||||||
if err := <-resp; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for tasks to finish
|
// Best-effort task drain: wait up to 30s for in-flight tasks to finish.
|
||||||
|
// Still done BEFORE ActionStop so getTaskLoad can round-trip through the
|
||||||
|
// manager loop.
|
||||||
timeout := time.NewTimer(30 * time.Second)
|
timeout := time.NewTimer(30 * time.Second)
|
||||||
defer timeout.Stop()
|
defer timeout.Stop()
|
||||||
out:
|
out:
|
||||||
@@ -487,8 +485,18 @@ out:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect from admin server
|
// Terminate the manager loop.
|
||||||
if adminClient := w.getAdmin(); adminClient != nil {
|
resp := make(chan error)
|
||||||
|
w.cmds <- workerCommand{
|
||||||
|
action: ActionStop,
|
||||||
|
resp: resp,
|
||||||
|
}
|
||||||
|
if err := <-resp; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect from admin server.
|
||||||
|
if adminClient != nil {
|
||||||
if err := adminClient.Disconnect(); err != nil {
|
if err := adminClient.Disconnect(); err != nil {
|
||||||
glog.Errorf("Error disconnecting from admin server: %v", err)
|
glog.Errorf("Error disconnecting from admin server: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user