This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git


The following commit(s) were added to refs/heads/main by this push:
     new 65d951f1c fix(fodc): keep retrying agent registration after proxy 
restart (#1163)
65d951f1c is described below

commit 65d951f1c464bd85770c51b4d69a0a28ed861a1e
Author: Gao Hongtao <[email protected]>
AuthorDate: Mon Jun 8 16:39:49 2026 +0800

    fix(fodc): keep retrying agent registration after proxy restart (#1163)
---
 fodc/agent/internal/proxy/client.go                | 70 ++++++++++++++++---
 .../internal/proxy/reconnect_schedule_test.go      | 79 ++++++++++++++++++++++
 2 files changed, 140 insertions(+), 9 deletions(-)

diff --git a/fodc/agent/internal/proxy/client.go 
b/fodc/agent/internal/proxy/client.go
index b18305caa..855194aca 100644
--- a/fodc/agent/internal/proxy/client.go
+++ b/fodc/agent/internal/proxy/client.go
@@ -44,6 +44,11 @@ import (
        "github.com/apache/skywalking-banyandb/pkg/run"
 )
 
+// defaultReconnectRetryInterval is the fallback delay scheduleReconnect uses 
when the client was
+// created without a positive reconnect interval. In practice 
c.reconnectInterval is always set from
+// the --reconnect-interval flag; this only guards a zero value.
+const defaultReconnectRetryInterval = 10 * time.Second
+
 // MetricsRequestFilter defines filters for metrics requests.
 type MetricsRequestFilter struct {
        StartTime *time.Time
@@ -68,6 +73,7 @@ type Client struct {
        lifecycleStream    fodcv1.FODCService_StreamLifecycleClient
        crashStream        fodcv1.FODCService_StreamCrashDiagnosticsClient
        client             fodcv1.FODCServiceClient
+       reconnectFn        func(context.Context) // overridable in tests; nil 
means use reconnect
 
        proxyAddr      string
        nodeRole       string
@@ -1335,10 +1341,11 @@ func (c *Client) reconnect(ctx context.Context) {
        }
 
        if connResult.err != nil {
-               c.logger.Error().Err(connResult.err).Msg("Failed to reconnect 
to Proxy")
-               if disconnectErr := c.Disconnect(); disconnectErr != nil {
-                       c.logger.Warn().Err(disconnectErr).Msg("Failed to 
disconnect")
-               }
+               // Transient failure (e.g. the proxy pod is mid-rollout). Do 
NOT call Disconnect here: that
+               // latches c.disconnected and the guard at the top of reconnect 
would suppress every future
+               // attempt. Schedule another try so the agent recovers once the 
proxy is ready again.
+               c.logger.Error().Err(connResult.err).Msg("Failed to reconnect 
to Proxy; will retry")
+               c.scheduleReconnect(ctx)
                return
        }
 
@@ -1348,31 +1355,41 @@ func (c *Client) reconnect(ctx context.Context) {
                c.streamsMu.Unlock()
        }
 
+       // Each stream (re)start below can fail transiently when the new proxy 
pod is not yet accepting
+       // RPCs. reconnect() has already torn down the heartbeat ticker and all 
stream receive-loops -
+       // the only mechanisms that would otherwise re-trigger a reconnect - so 
returning here without
+       // rescheduling leaves the agent permanently disconnected 
(agents_online drops to 0 until a
+       // manual pod restart). Always schedule another attempt on failure.
        if regErr := c.StartRegistrationStream(ctx); regErr != nil {
-               c.logger.Error().Err(regErr).Msg("Failed to restart 
registration stream")
+               c.logger.Error().Err(regErr).Msg("Failed to restart 
registration stream; will retry")
+               c.scheduleReconnect(ctx)
                return
        }
 
        if metricsErr := c.StartMetricsStream(ctx); metricsErr != nil {
-               c.logger.Error().Err(metricsErr).Msg("Failed to restart metrics 
stream")
+               c.logger.Error().Err(metricsErr).Msg("Failed to restart metrics 
stream; will retry")
+               c.scheduleReconnect(ctx)
                return
        }
 
        if clusterStateErr := c.StartClusterStateStream(ctx); clusterStateErr 
!= nil {
-               c.logger.Error().Err(clusterStateErr).Msg("Failed to restart 
cluster state stream")
+               c.logger.Error().Err(clusterStateErr).Msg("Failed to restart 
cluster state stream; will retry")
+               c.scheduleReconnect(ctx)
                return
        }
 
        if c.lifecycleCollector != nil {
                if lifecycleErr := c.StartLifecycleStream(ctx); lifecycleErr != 
nil {
-                       c.logger.Error().Err(lifecycleErr).Msg("Failed to 
restart lifecycle stream")
+                       c.logger.Error().Err(lifecycleErr).Msg("Failed to 
restart lifecycle stream; will retry")
+                       c.scheduleReconnect(ctx)
                        return
                }
        }
 
        if c.collectionLister != nil {
                if crashErr := c.StartCrashStream(ctx); crashErr != nil {
-                       c.logger.Error().Err(crashErr).Msg("Failed to restart 
crash diagnostics stream")
+                       c.logger.Error().Err(crashErr).Msg("Failed to restart 
crash diagnostics stream; will retry")
+                       c.scheduleReconnect(ctx)
                        return
                }
        }
@@ -1380,6 +1397,41 @@ func (c *Client) reconnect(ctx context.Context) {
        c.logger.Info().Msg("Successfully reconnected to Proxy")
 }
 
+// scheduleReconnect arms a delayed reconnect attempt after a transient 
failure during reconnect
+// (e.g. the proxy pod not yet ready), so the agent keeps retrying instead of 
stalling forever.
+// reconnect() is single-flight and re-checks ctx and c.disconnected, so a 
timer that fires after
+// shutdown is a harmless no-op. Each failed reconnect arms exactly one timer, 
so retries proceed at
+// roughly one per interval rather than in a tight loop.
+func (c *Client) scheduleReconnect(ctx context.Context) {
+       c.streamsMu.Lock()
+       disconnected := c.disconnected
+       interval := c.reconnectInterval
+       c.streamsMu.Unlock()
+
+       if disconnected {
+               return
+       }
+       if interval <= 0 {
+               interval = defaultReconnectRetryInterval
+       }
+
+       select {
+       case <-ctx.Done():
+               return
+       default:
+       }
+
+       reconnectFn := c.reconnect
+       if c.reconnectFn != nil {
+               reconnectFn = c.reconnectFn
+       }
+
+       c.logger.Info().Dur("retry_in", interval).Msg("Scheduling proxy 
reconnect retry")
+       time.AfterFunc(interval, func() {
+               reconnectFn(ctx)
+       })
+}
+
 // startHeartbeat starts the heartbeat ticker.
 func (c *Client) startHeartbeat(ctx context.Context) {
        c.streamsMu.Lock()
diff --git a/fodc/agent/internal/proxy/reconnect_schedule_test.go 
b/fodc/agent/internal/proxy/reconnect_schedule_test.go
new file mode 100644
index 000000000..a3a49b7eb
--- /dev/null
+++ b/fodc/agent/internal/proxy/reconnect_schedule_test.go
@@ -0,0 +1,79 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package proxy
+
+import (
+       "context"
+       "sync/atomic"
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+)
+
+// TestScheduleReconnect_ArmsRetryWhenHealthy is the regression guard for the 
reconnect-stall root
+// cause: a transient stream-establishment failure must arm another reconnect 
attempt rather than
+// leaving the agent permanently disconnected.
+func TestScheduleReconnect_ArmsRetryWhenHealthy(t *testing.T) {
+       var calls int32
+       c := &Client{
+               logger:            initTestLogger(t),
+               reconnectInterval: 20 * time.Millisecond,
+               reconnectFn:       func(context.Context) { 
atomic.AddInt32(&calls, 1) },
+       }
+
+       c.scheduleReconnect(context.Background())
+
+       assert.Eventually(t, func() bool {
+               return atomic.LoadInt32(&calls) == 1
+       }, time.Second, 5*time.Millisecond, "scheduleReconnect should arm 
exactly one retry")
+}
+
+// TestScheduleReconnect_SkipsWhenDisconnected confirms an intentional 
disconnect suppresses retries
+// so a shut-down agent does not keep dialing the proxy.
+func TestScheduleReconnect_SkipsWhenDisconnected(t *testing.T) {
+       var calls int32
+       c := &Client{
+               logger:            initTestLogger(t),
+               reconnectInterval: 10 * time.Millisecond,
+               disconnected:      true,
+               reconnectFn:       func(context.Context) { 
atomic.AddInt32(&calls, 1) },
+       }
+
+       c.scheduleReconnect(context.Background())
+
+       time.Sleep(60 * time.Millisecond)
+       assert.Equal(t, int32(0), atomic.LoadInt32(&calls), "no retry should be 
armed when disconnected")
+}
+
+// TestScheduleReconnect_SkipsWhenContextDone confirms a canceled context 
suppresses retries.
+func TestScheduleReconnect_SkipsWhenContextDone(t *testing.T) {
+       var calls int32
+       c := &Client{
+               logger:            initTestLogger(t),
+               reconnectInterval: 10 * time.Millisecond,
+               reconnectFn:       func(context.Context) { 
atomic.AddInt32(&calls, 1) },
+       }
+
+       ctx, cancel := context.WithCancel(context.Background())
+       cancel()
+       c.scheduleReconnect(ctx)
+
+       time.Sleep(60 * time.Millisecond)
+       assert.Equal(t, int32(0), atomic.LoadInt32(&calls), "no retry should be 
armed when context is canceled")
+}

Reply via email to