You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: core/consumer/consumer.go
+32-10Lines changed: 32 additions & 10 deletions
Original file line number
Diff line number
Diff line change
@@ -2,6 +2,7 @@ package consumer
2
2
3
3
import (
4
4
"context"
5
+
"errors"
5
6
"fmt"
6
7
"sync"
7
8
"time"
@@ -20,16 +21,23 @@ const (
20
21
21
22
// Consumer orchestrates multiple queue consumers. It handles subscription lifecycle,
22
23
// message consumption, ack/nack, and graceful shutdown for the entire pipeline.
24
+
// Start(), Register() and Stop() are always called in this order so they do not need to be concurrently-safe between
25
+
// one another, but the implementation must be thread-safe between message processing and Register()/Stop() operations.
23
26
typeConsumerinterface {
24
27
// Register adds a controller to the consumer. Must be called before Start().
25
28
Register(controllerController) error
26
29
27
30
// Start subscribes to all registered controllers' topics and begins consuming messages.
31
+
// Context is cancelled when the consumer is stopped, the implementation should propagate it to the controllers
32
+
// running message processing. The implementation can react immediately to the context cancellation by returning `ctx.Err()` instead of starting the message processing,
33
+
// but can also opt out to defer the cancellation after the message processing routine is set up.
34
+
// Start() will only be called once at the application startup, so it does not need to be idempotent.
28
35
Start(ctx context.Context) error
29
36
30
37
// Stop gracefully shuts down all controllers with the specified timeout.
31
38
// timeoutMs is the maximum time in milliseconds to wait for graceful shutdown.
32
39
// Returns error if shutdown times out.
40
+
// Stop() will only be called once at the application shutdown, so it does not need to be idempotent.
// By convention, Controller can only return context.Canceled if it is cancelled by the context, i.e. when consumer is stopped or application is shutting down
// The implementation of the controller should be idempotent and stateless. The controller is expected to be retried for the same message multiple times and should process side effects gracefully.
79
+
// The implementation must be thread-safe.
78
80
typeControllerinterface {
79
81
// Process processes a delivery. Controller receives consumer.Delivery (not extension/queue.Delivery)
80
82
// which prevents direct Ack/Nack calls - Consumer handles those automatically.
81
-
// Return nil to ack the message (success), error to nack and retry,
82
-
// or NonRetryableError to ack a poison pill message.
83
+
// Return nil to ack the message (success), error to nack and retry, or NonRetryableError to ack a poison pill message.
84
+
// Context controls the lifecycle of the service. It is cancelled when the consumer is stopped. The implementation should process it gracefully:
85
+
// - Pass the context to the underlying services and wait for them to complete their operations.
86
+
// - Proceed to the nearest safe state.
87
+
// - If the nearest safe state is not the final state of the controller, return `ctx.Err()` and the message would be nacked and retried.
88
+
// - If the nearest safe state is the final state of the controller, return either nil (if it is a successful state) or an actual error (and not `ctx.Err()`).
89
+
// It is strongly recommended to properly propagate the context all the way down to the long-running services and ensure they can process it "good enough", guided by the principles outlined above.
90
+
// Never return `context.Canceled` unless it is a result of a processing `ctx` cancellation!
| 1 | Startup failure or runtime error (details on stderr). |
59
+
| 143 | Stopped by signal (SIGINT or SIGTERM). This is 128 + SIGTERM per POSIX. |
60
+
61
+
When shutdown itself encounters errors (e.g. the gRPC server returns an error during graceful stop, or queue consumers time out), those override the signal exit code and the process exits with code 1. The actual errors are printed to stderr.
fmt.Fprintf(os.Stderr, "Gateway server failure: %v\n", err)
46
-
os.Exit(1)
47
+
iferrors.Is(err, context.Canceled) {
48
+
fmt.Println("Gateway server stopped by signal")
49
+
50
+
// Return 143 (128 + SIGTERM) as per POSIX standard if the application receives any termination signal from the OS. Ideally we should return 128+SIGINT for SIGINT and 128+SIGTERM for SIGTERM,
51
+
// but it will require a special processing not yet available in the standard library.
52
+
code=128+int(syscall.SIGTERM)
53
+
} else {
54
+
fmt.Fprintf(os.Stderr, "Gateway server failure: %v\n", err)
55
+
// TODO: classify errors and implement a binary protocol for exit codes, so far 1 for everything
56
+
code=1
57
+
}
47
58
}
59
+
os.Exit(code)
48
60
}
49
61
50
62
funcrun() error {
@@ -162,24 +174,36 @@ func run() error {
162
174
}
163
175
164
176
fmt.Printf("Gateway gRPC server is running on %s\n", port)
165
-
fmt.Println("Press Ctrl+C to stop.")
177
+
fmt.Println("Press Ctrl+C to stop, or send a SIGTERM.")
166
178
167
179
// Start server in a goroutine and wait for it to finish
168
180
serverErrCh:=make(chanerror, 1)
169
181
gofunc() {
170
182
serverErrCh<-grpcServer.Serve(listener)
171
183
}()
172
184
173
-
// Wait for interrupt signal or server exit
185
+
// Wait for interrupt signal or server critical error
186
+
// If interruption is signaled, gracefully stop the server
187
+
// If an error happens during shutdown, return the actual error, not the context cancellation error
188
+
varserverErrerror
174
189
select {
175
190
case<-ctx.Done():
176
-
fmt.Println("\nShutting down gateway server...")
191
+
fmt.Println("Shutting down gateway server due to interruption signal...")
192
+
193
+
// Set the error to the context cancellation error to be surfaced as a desired exit code by the main function
194
+
// to indicate that the server was stopped as intended
195
+
// It may be overridden by the server error if any
196
+
err=ctx.Err()
197
+
198
+
// stop GRPC server and wait for it to exit
177
199
grpcServer.GracefulStop()
178
-
_=<-serverErrCh// Wait for the server to exit and ignore the error
179
-
caseerrCh:=<-serverErrCh:
180
-
iferrCh!=nil {
181
-
err=fmt.Errorf("\nServer exited with error: %w\n", errCh)
182
-
}
200
+
serverErr=<-serverErrCh
201
+
caseserverErr=<-serverErrCh:
202
+
fmt.Println("Shutting down gateway server due to critical GRPC server error...")
203
+
}
204
+
205
+
ifserverErr!=nil {
206
+
err=fmt.Errorf("GRPC server exited with error: %w", serverErr)
0 commit comments