Documentation
¶
Overview ¶
Package queuebox is a pull-inbox worker for QueueBox.
QueueBox makes the push path need no code. The pull path needed five SQL statements, a renewal timer and a lease discipline, written by hand in every application. This package holds all of it.
The guarantee ¶
The application's writes and the completion commit together. The handler receives the transaction, and the library runs the completion inside it. If the completion affects no row, the lease was lost, another worker owns the message, and the library rolls the writes back and reports nothing as done.
Use ¶
worker, err := queuebox.NewInboxWorker(db, queuebox.Options{Source: "orders"})
if err != nil {
return err
}
err = worker.Run(ctx, func(ctx context.Context, message queuebox.Message, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, "INSERT INTO orders (id) VALUES ($1)", message.IdempotencyKey)
return err
})
Write every application change through the transaction the handler receives. Do not commit and do not roll back, because the library owns both. Return an error to fail the message. Honour the context: it is cancelled when the lease is lost and when a shutdown runs out of grace.
Databases ¶
PostgreSQL through pgx, and SQL Server through go-mssqldb. The library talks to database/sql and imports no driver, so the application registers the one it already uses. Both dialects bind positionally: PostgreSQL writes $1 and SQL Server writes @p1.
See the README for the settings, the retry policy, the shutdown behaviour and the schema mapping.
Index ¶
Constants ¶
const MaxErrorLength = 2000
MaxErrorLength is the longest text that Sanitize returns.
Variables ¶
This section is empty.
Functions ¶
func Sanitize ¶
Sanitize redacts every secret value in the text and truncates the result. It prepares an error text for the last_error column and for a log line.
func SanitizeError ¶
SanitizeError names the failure and every cause below it, then redacts the whole text.
A driver puts the connection string in the message of the cause, not of the wrapper. The chain therefore reaches the redaction, and no cause message escapes it.
Types ¶
type FailureAction ¶
type FailureAction struct {
// Retry is true to run the retry statement, and false to run the dead-letter statement.
Retry bool
// Delay is how long the retry waits before the message can be claimed again.
Delay time.Duration
}
FailureAction states what the library does with a message whose handler returned an error.
func DeadLetter ¶
func DeadLetter() FailureAction
DeadLetter moves the message to the dead letter and stops.
func RetryAfter ¶
func RetryAfter(delay time.Duration) FailureAction
RetryAfter returns the message to pending after a delay.
type Handler ¶
Handler handles one message.
The transaction is the point of the pull path. Write every application change through it. The library runs the completion in the same transaction after the handler returns, so the writes and the completion commit together or neither of them does.
Do not commit and do not roll back. The library owns both.
The context is cancelled when the lease is lost and when a shutdown runs out of grace.
type InboxWorker ¶
type InboxWorker struct {
// contains filtered or unexported fields
}
InboxWorker claims QueueBox pull messages, hands each one to a handler inside a transaction, and completes, retries or dead-letters it.
func NewInboxWorker ¶
func NewInboxWorker(db *sql.DB, options Options) (*InboxWorker, error)
NewInboxWorker builds a worker.
database/sql is the abstraction, so the library needs no driver of its own and one worker serves the PostgreSQL and the SQL Server drivers alike.
func (*InboxWorker) Run ¶
func (w *InboxWorker) Run(ctx context.Context, handler Handler) error
Run works until the context is cancelled.
A cancellation stops the claiming at once. The handlers that already run keep their grace, and a handler that does not finish inside it is cancelled and its message is abandoned. An abandoned message is never completed; its lease expires and another worker takes it.
type Logger ¶
type Logger interface {
Info(message string, fields map[string]any)
Warn(message string, fields map[string]any)
Error(message string, fields map[string]any)
}
Logger is the logger the library writes to. It prints nothing without one.
type Message ¶
type Message struct {
// ID is the inbox row identifier.
ID string
// Source is the source name.
Source string
// IdempotencyKey is the deduplication key. The full identity is the source and this key
// together, because two sources can send the same event ID and mean different events.
IdempotencyKey string
// AggregateID is nullable.
AggregateID *string
// EventType is nullable.
EventType *string
// Payload is the JSON body, as it was stored.
Payload json.RawMessage
// Attempt is the delivery counter. It is zero on the first delivery.
Attempt int
// CorrelationID is nullable, and it is for logs.
CorrelationID *string
// Headers holds the headers the source received, one value per key. It is never nil.
Headers map[string]string
}
Message is one claimed inbox row, as the handler sees it.
The claim token is deliberately absent. The library owns the token, because a handler that could reach it could complete a message out of band.
func (Message) UnmarshalPayload ¶
UnmarshalPayload parses the JSON body into the value that target points at.
type Options ¶
type Options struct {
// Source is the source whose messages this worker takes. It is mandatory.
Source string
// BatchSize is the largest number of messages one claim takes. The default is ten.
BatchSize int
// LeaseMS is the lease duration in milliseconds. The renewal runs every third of it.
// The default is thirty thousand.
LeaseMS int
// MaxConcurrency is the largest number of handlers that run at one time. It never passes the
// batch size. The default is one. A handler meets no sibling message unless the caller asks
// for parallelism.
MaxConcurrency int
// PollInterval is how long the worker waits after a claim that returned nothing.
// The default is one second.
PollInterval time.Duration
// ShutdownGrace is how long a stop waits for the handlers that already run.
// The default is thirty seconds.
ShutdownGrace time.Duration
// Dialect is the database dialect. The default is PostgreSQL.
Dialect Dialect
// Schema names the table and the columns. The default is the QueueBox V6 names.
Schema *Schema
// RetryPolicy decides what happens to a message whose handler returned an error.
RetryPolicy RetryPolicy
// Logger is the caller's logger.
Logger Logger
}
Options holds the settings of one worker.
type RetryPolicy ¶
type RetryPolicy interface {
Decide(message Message, failure error) FailureAction
}
RetryPolicy decides what happens to a message whose handler returned an error.
The library exposes the decision, because only the application knows that a validation error must never be retried while a timeout must.
func NewDefaultRetryPolicy ¶
func NewDefaultRetryPolicy(maxAttempts int, baseDelay, maxDelay time.Duration, jitter float64) RetryPolicy
NewDefaultRetryPolicy retries while the attempt is below the ceiling, with an exponential backoff and jitter, and dead-letters after that.
The row starts at attempt zero, so the ceiling allows maxAttempts plus one deliveries.
type RetryPolicyFunc ¶
type RetryPolicyFunc func(message Message, failure error) FailureAction
RetryPolicyFunc lets a plain function be a RetryPolicy.
func (RetryPolicyFunc) Decide ¶
func (f RetryPolicyFunc) Decide(message Message, failure error) FailureAction
Decide calls the function.
type Schema ¶
type Schema struct {
Table string
ID string
Consumption string
Source string
State string
ScheduledAt string
CreatedAt string
ClaimToken string
ClaimedAt string
LeaseExpiresAt string
ProcessedAt string
Attempt string
LastError string
IdempotencyKey string
AggregateID string
EventType string
Payload string
CorrelationID string
Headers string
}
Schema names the inbox table and every column the library reads or writes. QueueBox lets an operator map these names, so the library must not assume the defaults.
type Statements ¶
type Statements struct {
// Claim takes rows. Parameters: source, batch, leaseMS, candLimit.
//
// The SQL Server text carries its own transaction control (BEGIN TRANSACTION and COMMIT
// TRANSACTION). The claim must be alone in that transaction: put no other application work
// in it, and commit it before starting any handler. A wider transaction around the claim
// changes three things silently: the claimed rows stay uncommitted until the wider
// transaction commits, a lock failure rolls back that whole wider transaction instead of
// only the claim, and the per-source applock stays held for as long as the wider transaction
// stays open instead of releasing at the claim's own commit.
Claim string
// Renew extends the lease. Parameters: leaseMS, id, token.
Renew string
// Complete marks the row processed. Parameters: id, token.
Complete string
// Retry returns the row to pending. Parameters: delayMS, error, id, token.
Retry string
// Dead marks the row dead. Parameters: error, id, token.
Dead string
}
Statements holds the five statements of the pull contract, rendered for one dialect and one schema.
Both dialects bind POSITIONALLY, because the PostgreSQL driver accepts no named parameter. PostgreSQL writes $1 and SQL Server writes @p1, so one database/sql call site serves both.
func NewStatements ¶
func NewStatements(dialect Dialect, schema Schema) (Statements, error)
NewStatements renders the five statements of the pull contract.
The library uses it, and an application that wants to run one statement by hand can use it too. Every identifier is quoted and checked, so a schema mapping cannot carry SQL.
A caller who runs the SQL Server Claim by hand must run it alone, never nested inside a wider transaction: see the Claim field for the three consequences of nesting it.