@@ -19,7 +19,6 @@ import (
19
19
"fmt"
20
20
"net/http"
21
21
"strings"
22
- "sync"
23
22
"sync/atomic"
24
23
"time"
25
24
@@ -33,16 +32,6 @@ import (
33
32
"github.com/bucketeer-io/bucketeer/v2/pkg/token"
34
33
)
35
34
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
-
46
35
type Server struct {
47
36
certPath string
48
37
keyPath string
@@ -55,7 +44,7 @@ type Server struct {
55
44
handlers []httpHandler
56
45
rpcServer * grpc.Server
57
46
httpServer * http.Server
58
- grpcWebServer * grpcweb.WrappedGrpcServer
47
+ grpcWebServer * grpcweb.WrappedGrpcServer // DEPRECATED: Remove once Node.js SDK migrates away from grpc-web
59
48
readTimeout time.Duration
60
49
writeTimeout time.Duration
61
50
idleTimeout time.Duration
@@ -157,81 +146,42 @@ func (s *Server) Stop(timeout time.Duration) {
157
146
zap .String ("server" , s .name ),
158
147
zap .Duration ("timeout" , timeout ))
159
148
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.
162
155
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
193
157
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" )
210
159
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
+ }
227
168
}
228
169
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" )
233
179
}
234
180
181
+ s .logger .Info ("Server shutdown completed" ,
182
+ zap .String ("server" , s .name ),
183
+ zap .Duration ("total_duration" , time .Since (shutdownStart )))
184
+
235
185
// Mark shutdown as complete for Envoy coordination
236
186
atomic .StoreInt32 (& s .shutdownComplete , 1 )
237
187
s .logger .Info ("Shutdown complete flag set, Envoy can now terminate" )
@@ -260,7 +210,9 @@ func (s *Server) setupRPC() {
260
210
service .Register (s .rpcServer )
261
211
}
262
212
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.
264
216
s .grpcWebServer = grpcweb .WrapServer (s .rpcServer )
265
217
}
266
218
@@ -287,6 +239,8 @@ func (s *Server) setupHTTP() {
287
239
288
240
// Wrap the main handler with shutdown tracking middleware
289
241
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
290
244
if s .grpcWebServer .IsGrpcWebRequest (req ) {
291
245
s .grpcWebServer .ServeHTTP (resp , req )
292
246
} else if isRPC (req ) {
0 commit comments