Shutdown
producer.Close(ctx) is what makes the last batch arrive. A process that exits without it loses whatever was buffered, which on the defaults is up to five seconds of events.
package main
import ( "context" "errors" "log" "time"
"github.com/kataras/patrol")
func main() { producer := patrol.NewProducer(patrol.ProducerOptions{ProjectName: "checkout-api"})
serve(producer)
// Give delivery ten seconds, then leave. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel()
if err := producer.Close(ctx); err != nil { log.Println("patrol: shutdown did not finish in time:", err) }
if err := producer.SendEvent(ctx, patrol.NewEvent(patrol.Info).WithMessage("too late")); !errors.Is(err, patrol.ErrClosed) { log.Println("expected ErrClosed, got:", err) }}
func serve(*patrol.Producer) {}What Close does, in order
Section titled “What Close does, in order”- Stops new
Debuggingsessions. One already running is allowed to finish. - Waits for every active session.
- Closes the event channel.
SendEventreturnspatrol.ErrClosedfrom this point on. - The background loop drains what is left, flushes it once through every flusher, and returns.
- Every flusher that implements
Closergets itsClose(ctx). An error there goes toOnErrorassql: close: ....
Close returns nil once that sequence finishes. Delivery errors are not its return value: they go to OnError, like every other flush error, because one of six flushers failing is not a reason for a shutdown to report failure.
When the deadline passes first
Section titled “When the deadline passes first”Close returns ctx.Err() and cancels the in-flight flush. The shutdown itself keeps going in the background: the buffer still drains and the Closers still run, with a cancelled context. Your process is free to exit at that point, and a flusher that respects context cancellation will stop early.
Pass a context with a deadline in a service that has to exit on time, and context.Background() in a short program where correctness beats the clock.
Close is safe to call more than once and from more than one goroutine. The second call waits on the same shutdown as the first.
ErrClosed
Section titled “ErrClosed”patrol.ErrClosed is returned by SendEvent after the channel is closed and by Debugging once Close has started. Compare it with errors.Is. It means the producer is gone, not that the event was bad: nothing you can retry on the same producer will work.
There is no reopen. A producer is created by NewProducer and ends at Close.
Test, and ErrTest
Section titled “Test, and ErrTest”producer.Test(ctx) is the other half of the lifecycle, at the start of it. It calls Test(ctx) on every flusher that implements Tester, concurrently, and returns their errors joined and prefixed with the flusher names. A flusher with no Test is skipped without complaint.
Every built-in Test wraps patrol.ErrTest, so errors.Is(err, patrol.ErrTest) is true for a failed connectivity check and false for anything else that went wrong on the way:
| Flusher | What Test does |
|---|---|
| Slack | auth.test, and checks a bot id came back |
| Discord | Reads every configured channel with the bot token |
| Twilio | Lists one active account |
| Opens an SMTP session, upgrades to TLS, authenticates, quits | |
| SQL | Pings the database |
| HTTP client | GET / against the server, which also checks the credentials |
Each of those is a real round trip to someone else’s service. Call it at startup and log the result; a service that exits because Slack is having a bad morning is worse off than one that logs it and carries on.
Next: stacktraces.