63 lines
2.0 KiB
Go
63 lines
2.0 KiB
Go
// Package queue defines durable, at-least-once work delivery. Handlers must be
|
|
// idempotent: a crash after a business commit but before ACK can cause redelivery.
|
|
// This is a message queue, not a workflow engine or a distributed transaction.
|
|
package queue
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
var ErrFull = errors.New("queue: capacity reached")
|
|
var ErrConflict = errors.New("queue: message ID already has different content")
|
|
var ErrLeaseLost = errors.New("queue: delivery ownership changed")
|
|
|
|
type Message struct {
|
|
ID string `json:"id"` // Stable producer identity, saved before submitting.
|
|
Name string `json:"name"`
|
|
Payload json.RawMessage `json:"payload"`
|
|
}
|
|
type Receipt struct {
|
|
ID string `json:"id"`
|
|
BackendID string `json:"backendId"`
|
|
}
|
|
type Delivery struct {
|
|
Message
|
|
Attempt int64 `json:"attempt"`
|
|
}
|
|
type Handler func(context.Context, Delivery) error
|
|
type Publisher interface {
|
|
Publish(context.Context, Message) (Receipt, error)
|
|
}
|
|
type Consumer interface {
|
|
Run(context.Context, Handler) error
|
|
}
|
|
|
|
// Reject marks a known permanent failure; it is retained for operator review.
|
|
// Ordinary errors are retried up to the consumer's configured attempt limit.
|
|
type Rejection struct{ Code string }
|
|
|
|
func (e *Rejection) Error() string { return "queue: rejected: " + e.Code }
|
|
func Reject(code string) error { return &Rejection{Code: code} }
|
|
|
|
func Validate(ctx context.Context, m Message, maxBytes int) error {
|
|
if ctx == nil {
|
|
return errors.New("queue: context required")
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
for _, v := range []string{m.ID, m.Name} {
|
|
if v == "" || len(v) > 128 || strings.IndexFunc(v, func(r rune) bool { return r < 33 || r > 126 }) >= 0 {
|
|
return errors.New("queue: ID and name must be 1..128 printable ASCII characters")
|
|
}
|
|
}
|
|
if len(m.Payload) > maxBytes || !json.Valid(m.Payload) {
|
|
return fmt.Errorf("queue: payload must be valid JSON within %d bytes", maxBytes)
|
|
}
|
|
return nil
|
|
}
|