@@ -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-
4635type 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