Skip to content

Commit fdb962a

Browse files
committed
fix: shutdown order
Signed-off-by: Alessandro Yuichi Okimoto <[email protected]>
1 parent b66f813 commit fdb962a

File tree

3 files changed

+39
-81
lines changed

3 files changed

+39
-81
lines changed

manifests/bucketeer/charts/api/templates/envoy-configmap.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ data:
201201
- name: envoy.filters.http.cors
202202
typed_config:
203203
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
204+
# DEPRECATED: grpc-web filter for legacy Node.js SDK
205+
# TODO: Remove once Node.js SDK migrates to gRPC-Gateway (REST) or pure gRPC
204206
- name: envoy.filters.http.grpc_web
205207
typed_config:
206208
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb

manifests/bucketeer/charts/web/templates/envoy-configmap.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,8 @@ data:
779779
- name: envoy.filters.http.cors
780780
typed_config:
781781
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
782+
# DEPRECATED: grpc-web filter for legacy Node.js SDK
783+
# TODO: Remove once Node.js SDK migrates to gRPC-Gateway (REST) or pure gRPC
782784
- name: envoy.filters.http.grpc_web
783785
typed_config:
784786
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb

pkg/rpc/server.go

Lines changed: 35 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"fmt"
2020
"net/http"
2121
"strings"
22-
"sync"
2322
"sync/atomic"
2423
"time"
2524

@@ -33,16 +32,6 @@ import (
3332
"github.com/bucketeer-io/bucketeer/v2/pkg/token"
3433
)
3534

36-
const (
37-
// minGracefulTimeout is the minimum timeout for graceful shutdown operations.
38-
// This ensures that shutdown operations have at least some time to complete.
39-
minGracefulTimeout = 1 * time.Second
40-
41-
// httpServerBuffer is the additional time reserved for HTTP server shutdown
42-
// after gRPC server completes. This prevents premature HTTP termination.
43-
httpServerBuffer = 1 * time.Second
44-
)
45-
4635
type Server struct {
4736
certPath string
4837
keyPath string
@@ -55,7 +44,7 @@ type Server struct {
5544
handlers []httpHandler
5645
rpcServer *grpc.Server
5746
httpServer *http.Server
58-
grpcWebServer *grpcweb.WrappedGrpcServer
47+
grpcWebServer *grpcweb.WrappedGrpcServer // DEPRECATED: Remove once Node.js SDK migrates away from grpc-web
5948
readTimeout time.Duration
6049
writeTimeout time.Duration
6150
idleTimeout time.Duration
@@ -157,81 +146,42 @@ func (s *Server) Stop(timeout time.Duration) {
157146
zap.String("server", s.name),
158147
zap.Duration("timeout", timeout))
159148

160-
var wg sync.WaitGroup
161-
shutdownErrors := make(chan error, 2)
149+
// Shutdown order is critical:
150+
// 1. HTTP server first (drains REST/gRPC-Gateway requests)
151+
// 2. gRPC server second (only pure gRPC connections remain)
152+
//
153+
// This ensures HTTP requests that call s.rpcServer.ServeHTTP() can complete
154+
// before we stop the underlying gRPC server.
162155

163-
// Shutdown gRPC server
164-
if s.rpcServer != nil {
165-
wg.Add(1)
166-
go func() {
167-
defer wg.Done()
168-
s.logger.Info("Starting gRPC server graceful shutdown")
169-
170-
// Use a shorter timeout for gRPC to leave time for HTTP
171-
grpcTimeout := timeout - httpServerBuffer
172-
if grpcTimeout < minGracefulTimeout {
173-
grpcTimeout = minGracefulTimeout
174-
}
175-
176-
done := make(chan struct{})
177-
go func() {
178-
s.rpcServer.GracefulStop()
179-
close(done)
180-
}()
181-
182-
select {
183-
case <-done:
184-
s.logger.Info("gRPC server shutdown completed gracefully")
185-
case <-time.After(grpcTimeout):
186-
s.logger.Warn("gRPC server graceful shutdown timed out, forcing stop")
187-
s.rpcServer.Stop()
188-
}
189-
}()
190-
}
191-
192-
// Shutdown HTTP server
156+
// Step 1: Shutdown HTTP server gracefully
193157
if s.httpServer != nil {
194-
wg.Add(1)
195-
go func() {
196-
defer wg.Done()
197-
s.logger.Info("Starting HTTP server graceful shutdown")
198-
199-
ctx, cancel := context.WithTimeout(context.Background(), timeout)
200-
defer cancel()
201-
202-
if err := s.httpServer.Shutdown(ctx); err != nil {
203-
s.logger.Error("HTTP server failed to shut down gracefully", zap.Error(err))
204-
shutdownErrors <- err
205-
} else {
206-
s.logger.Info("HTTP server shutdown completed gracefully")
207-
}
208-
}()
209-
}
158+
s.logger.Info("Starting HTTP server graceful shutdown")
210159

211-
// Wait for all shutdowns to complete or timeout
212-
done := make(chan struct{})
213-
go func() {
214-
wg.Wait()
215-
close(done)
216-
}()
217-
218-
select {
219-
case <-done:
220-
s.logger.Info("Server shutdown completed",
221-
zap.String("server", s.name),
222-
zap.Duration("total_duration", time.Since(shutdownStart)))
223-
case <-time.After(timeout):
224-
s.logger.Warn("Server shutdown timed out",
225-
zap.String("server", s.name),
226-
zap.Duration("timeout", timeout))
160+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
161+
defer cancel()
162+
163+
if err := s.httpServer.Shutdown(ctx); err != nil {
164+
s.logger.Error("HTTP server failed to shut down gracefully", zap.Error(err))
165+
} else {
166+
s.logger.Info("HTTP server shutdown completed gracefully")
167+
}
227168
}
228169

229-
// Log any shutdown errors
230-
close(shutdownErrors)
231-
for err := range shutdownErrors {
232-
s.logger.Error("Shutdown error", zap.Error(err))
170+
// Step 2: Stop gRPC server
171+
// Note: We use Stop() instead of GracefulStop() because:
172+
// - GracefulStop() panics on HTTP-served connections (serverHandlerTransport.Drain not implemented)
173+
// - HTTP-served connections were already drained in step 1
174+
// - Pure gRPC clients have retry logic and Envoy connection draining to handle this
175+
if s.rpcServer != nil {
176+
s.logger.Info("Stopping gRPC server")
177+
s.rpcServer.Stop()
178+
s.logger.Info("gRPC server stopped")
233179
}
234180

181+
s.logger.Info("Server shutdown completed",
182+
zap.String("server", s.name),
183+
zap.Duration("total_duration", time.Since(shutdownStart)))
184+
235185
// Mark shutdown as complete for Envoy coordination
236186
atomic.StoreInt32(&s.shutdownComplete, 1)
237187
s.logger.Info("Shutdown complete flag set, Envoy can now terminate")
@@ -260,7 +210,9 @@ func (s *Server) setupRPC() {
260210
service.Register(s.rpcServer)
261211
}
262212

263-
// Create gRPC-Web wrapper
213+
// DEPRECATED: grpc-web support for legacy Node.js SDK
214+
// TODO: Remove once Node.js SDK migrates to gRPC-Gateway (REST) or pure gRPC
215+
// This is an abandoned library (last updated 2021) with known issues.
264216
s.grpcWebServer = grpcweb.WrapServer(s.rpcServer)
265217
}
266218

@@ -287,6 +239,8 @@ func (s *Server) setupHTTP() {
287239

288240
// Wrap the main handler with shutdown tracking middleware
289241
mainHandler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
242+
// DEPRECATED: grpc-web support for legacy Node.js SDK
243+
// This check should be removed once Node.js SDK migrates away from grpc-web
290244
if s.grpcWebServer.IsGrpcWebRequest(req) {
291245
s.grpcWebServer.ServeHTTP(resp, req)
292246
} else if isRPC(req) {

0 commit comments

Comments
 (0)