mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-01 02:59:08 +02:00
[client] Make netbird up wait for the daemon to become ready
The CLI up path only tolerated a not-yet-ready daemon via a 10s blocking dial, so "netbird service start" immediately followed by "netbird up" (e.g. a container entrypoint) failed with a generic "daemon not running" error. The container entrypoint worked around this with a shell poll loop (status --check live) before running up. Move the readiness wait into the CLI, mirroring how the GUI already dials: - DialClientGRPCServer now uses grpc.NewClient with a tuned reconnect backoff and waits for the connection to reach READY (retrying on TRANSIENT_FAILURE) up to a 30s deadline, instead of grpc.DialContext + WithBlock with a hard 10s timeout. - up now polls Status via waitForDaemonStatus, retrying while the RPC is Unavailable (socket up but service not yet registered). Add an explicit daemon-ready signal so clients can wait deterministically instead of heuristically: - New optional StatusResponse.daemonReady field (field 5, wire-compatible with older GUIs/daemons which leave it unset). Regenerated with the pinned protoc v33.1 toolchain so no version churn leaks into the diff. - The server sets ready once Start succeeds and the DaemonService is registered (SetReady, called from the service controller). - waitForDaemonStatus waits for daemonReady=true (or an already-Connected status), with a bounded grace window so older daemons that never set the field are not blocked. Simplify the container entrypoint accordingly: drop the readiness poll loop (up now waits) and the now-dead NB_ENTRYPOINT_SERVICE_TIMEOUT env, keeping only the daemon+up process glue and SIGTERM forwarding for clean shutdown.
This commit is contained in:
+62
-7
@@ -20,6 +20,8 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/grpc"
|
||||
gbackoff "google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
daddr "github.com/netbirdio/netbird/client/internal/daemonaddr"
|
||||
@@ -264,17 +266,70 @@ func FlagNameToEnvVar(cmdFlag string, prefix string) string {
|
||||
return prefix + upper
|
||||
}
|
||||
|
||||
// DialClientGRPCServer returns client connection to the daemon server.
|
||||
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
|
||||
defer cancel()
|
||||
// defaultDaemonDialTimeout is how long DialClientGRPCServer waits for the daemon
|
||||
// to become reachable. It is intentionally generous so that invoking the CLI
|
||||
// right after "netbird service start" (e.g. from a container entrypoint) tolerates
|
||||
// the window where the daemon has created its socket but is not yet serving.
|
||||
const defaultDaemonDialTimeout = 30 * time.Second
|
||||
|
||||
return grpc.DialContext(
|
||||
ctx,
|
||||
// DialClientGRPCServer returns a client connection to the daemon server. It waits
|
||||
// for the daemon to become reachable, retrying with backoff until the connection
|
||||
// reports READY or defaultDaemonDialTimeout elapses. This handles the startup race
|
||||
// where the daemon socket exists (or is about to) before the gRPC server is serving.
|
||||
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
|
||||
return dialClientGRPCServer(ctx, addr, defaultDaemonDialTimeout)
|
||||
}
|
||||
|
||||
func dialClientGRPCServer(ctx context.Context, addr string, timeout time.Duration) (*grpc.ClientConn, error) {
|
||||
conn, err := grpc.NewClient(
|
||||
strings.TrimPrefix(addr, "tcp://"),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithBlock(),
|
||||
// Cap reconnect backoff at 5s; gRPC's default 120s MaxDelay would leave the
|
||||
// CLI waiting far too long to notice a freshly-started daemon. Mirrors the GUI.
|
||||
grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: gbackoff.Config{
|
||||
BaseDelay: 1 * time.Second,
|
||||
Multiplier: 1.6,
|
||||
Jitter: 0.2,
|
||||
MaxDelay: 5 * time.Second,
|
||||
},
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create daemon gRPC client: %w", err)
|
||||
}
|
||||
|
||||
// grpc.NewClient is lazy: it does not connect until the first RPC or until we
|
||||
// nudge it. Trigger connection attempts and wait until the channel reaches READY.
|
||||
if err := waitForConnReady(ctx, conn, timeout); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// waitForConnReady drives the gRPC channel out of IDLE and blocks until it becomes
|
||||
// READY, or until timeout/ctx expires. TRANSIENT_FAILURE (daemon not yet serving)
|
||||
// is treated as retryable so the caller keeps waiting within the deadline.
|
||||
func waitForConnReady(ctx context.Context, conn *grpc.ClientConn, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
state := conn.GetState()
|
||||
switch state {
|
||||
case connectivity.Ready:
|
||||
return nil
|
||||
case connectivity.Idle:
|
||||
// Kick the lazy channel into connecting.
|
||||
conn.Connect()
|
||||
}
|
||||
|
||||
if !conn.WaitForStateChange(ctx, state) {
|
||||
// ctx expired while in `state`.
|
||||
return fmt.Errorf("timed out after %s waiting for daemon to become ready (last state: %s)", timeout, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithBackOff execute function in backoff cycle.
|
||||
|
||||
Reference in New Issue
Block a user