Documentation
¶
Overview ¶
Package postmark is a production-like example application built on Conveyor: a miniature transactional email and notification platform. An API (the Producer) accepts requests to notify users, and every piece of downstream work is a Conveyor task processed by the handlers in this package.
The point of the example is that each Conveyor feature falls out of the product naturally rather than being bolted on. Password resets and 2FA codes ride a heavily weighted transactional queue; campaign blasts ride a lightly weighted marketing queue; a fake email provider with a fixed connection limit and an occasional outage exercises concurrency limits, retries with backoff, dead-lettering, and the per-task-type circuit breaker.
The worker and producer run as separate processes (cmd/worker and cmd/producer) against a Postgres-backed conveyord cluster; the deploy directory runs them on Kubernetes, which is how the example showcases Conveyor's durability and high-availability story. The send is always simulated, never real SMTP, so the example needs no secrets and the flaky and outage behaviors stay controllable for the retry, circuit-breaker, and dead-letter demos.
Index ¶
- Constants
- Variables
- func BounceAddress(userID int) string
- func DeliverableAddress(userID int) string
- func IsHardBounce(addr string) bool
- func NewMux(provider *Provider, logger *slog.Logger) *conveyor.Mux
- func WorkerQueues() map[string]int
- type Email
- type Producer
- func (p *Producer) Campaign(ctx context.Context, tenant string) error
- func (p *Producer) PasswordReset(ctx context.Context, userID int) error
- func (p *Producer) Receipt(ctx context.Context, userID int) error
- func (p *Producer) ResendStorm(ctx context.Context, userID int) error
- func (p *Producer) Run(ctx context.Context, interval time.Duration) error
- func (p *Producer) TwoFactor(ctx context.Context, userID int) error
- func (p *Producer) Welcome(ctx context.Context, userID int) error
- type Provider
- type ProviderConfig
- type ProviderStats
Constants ¶
const ( // QueueTransactional carries must-send-now mail: password resets, 2FA // codes, and receipts. It is weighted the heaviest. QueueTransactional = "transactional" // QueueDefault carries ordinary notifications: welcome mail and trial // reminders. It is weighted in between. QueueDefault = "default" // QueueMarketing carries campaign blasts. It is weighted the lightest so a // large send drains slowly without starving the other tiers. QueueMarketing = "marketing" )
The platform's queue tiers. A worker declares a relative weight per queue and the server hands out each queue's work in proportion, so a password reset never waits behind a million-recipient newsletter.
const ( // TaskWelcome is a welcome email sent immediately after signup. TaskWelcome = "email:welcome" // TaskPasswordReset is a password-reset email, deduplicated per user so a // burst of "resend" clicks does not send ten mails. TaskPasswordReset = "email:password-reset" // TaskTwoFactor is a 2FA code: the most urgent mail, jumping ahead of // everything else in the transactional queue and bounded by a tight timeout. TaskTwoFactor = "email:2fa" // TaskReceipt is a purchase receipt, kept visible after completion for the // audit view. TaskReceipt = "email:receipt" // TaskTrialEnding is a "your trial ends soon" reminder, scheduled for a // future time rather than sent now. TaskTrialEnding = "email:trial-ending" // TaskCampaign is one recipient of a marketing campaign blast. TaskCampaign = "email:campaign" // TaskDigest builds and sends every user's weekly activity summary; it is // materialized by a cron entry, not enqueued by the producer. TaskDigest = "digest:weekly" )
Task types routed to the handlers registered by NewMux.
const ( // PriorityUrgent puts 2FA codes ahead of everything in their queue. PriorityUrgent = 9 // PriorityHigh puts password resets ahead of ordinary transactional mail. PriorityHigh = 7 // PriorityBulk sinks campaign mail below interactive notifications. PriorityBulk = 2 )
Dispatch priorities within a queue (1 lowest, 9 highest; the unset default is 4). A 2FA code outranks a welcome email sharing the transactional queue.
Variables ¶
var ( // ErrTransient is a temporary failure (the provider's flaky "500"): the send // should be retried. ErrTransient = errors.New("postmark: provider returned a transient error") // ErrHardBounce is a permanent failure: the address is undeliverable and the // task must be archived rather than retried. ErrHardBounce = errors.New("postmark: recipient address hard-bounced") // ErrProviderDown is returned by every call while the provider is in an // outage. A task type that sees it repeatedly trips its circuit breaker. ErrProviderDown = errors.New("postmark: provider is unavailable") )
Provider errors. A handler maps each to a Conveyor outcome: a hard bounce is archived (SkipRetry), while a transient failure or a full outage is retried.
Functions ¶
func BounceAddress ¶
BounceAddress returns a permanently undeliverable address for a user, used to demonstrate hard bounces landing in the archive.
func DeliverableAddress ¶
DeliverableAddress returns the ordinary, deliverable address for a user.
func IsHardBounce ¶
IsHardBounce reports whether addr is permanently undeliverable. A handler that gets a hard bounce wraps the failure in conveyor.SkipRetry so the task is archived immediately instead of retried against an address that never works.
func NewMux ¶
NewMux builds the task router for a Postmark worker: every send task type is served by the same send handler (so each gets its own circuit breaker keyed by type), the weekly digest gets its own, and a logging middleware records the outcome of every task. The provider is the simulated email backend the handlers deliver through.
func WorkerQueues ¶
WorkerQueues returns the queue-to-weight map a Postmark worker serves: transactional far above default, marketing far below, so dispatch favors the mail that must go now. Pass it to conveyor.WithQueues.
Types ¶
type Email ¶
type Email struct {
// UserID identifies the recipient account; it keys per-user uniqueness.
UserID int `json:"user_id"`
// To is the destination address. An address at the hard-bounce domain is
// permanently undeliverable; see IsHardBounce.
To string `json:"to"`
// Subject is the human-readable subject line, used only for log output.
Subject string `json:"subject"`
// Tenant identifies the sending customer; it keys per-tenant send
// concurrency on campaign blasts. Empty for system mail.
Tenant string `json:"tenant,omitempty"`
}
Email is the payload of every send task: who to mail, on whose behalf, and what about. A single shape serves every task type because, to the platform, they are all "send this user an email".
type Producer ¶
type Producer struct {
// contains filtered or unexported fields
}
Producer simulates the customer apps hitting the platform's API. Each method enqueues the Conveyor tasks one product action generates; Run drives a believable continuous mix of them.
func NewProducer ¶
NewProducer builds a Producer over an enqueueing client.
func (*Producer) Campaign ¶
Campaign enqueues one marketing blast for a tenant: campaignRecipients recipients on the lightly weighted marketing queue at bulk priority, all sharing the tenant's concurrency key so one campaign cannot stampede the provider. A small fraction of recipients are at a hard-bounce address and will dead-letter.
func (*Producer) PasswordReset ¶
PasswordReset enqueues a password-reset email on the transactional queue, deduplicated per user: a second reset for the same user while the first is still pending is rejected as a duplicate, not sent twice.
func (*Producer) Receipt ¶
Receipt enqueues a purchase receipt on the transactional queue and keeps the completed task visible for the audit view via retention.
func (*Producer) ResendStorm ¶
ResendStorm simulates an impatient user mashing "resend" on the reset form. Every click after the first collides with the per-user uniqueness key and is dropped, so the user gets one mail instead of resendStormClicks.
func (*Producer) Run ¶
Run drives a continuous, transactional-heavy workload, enqueuing one product action every interval until ctx is canceled. It returns ctx.Err on exit.
type Provider ¶
type Provider struct {
// contains filtered or unexported fields
}
Provider is a fake email/SMTP provider. It accepts a bounded number of concurrent sends, fails a fraction of them transiently, permanently rejects hard-bounce addresses, and can be switched into a total outage. It holds no real connection and sends no real mail, so the example runs offline and its failure modes stay fully controllable.
func NewProvider ¶
func NewProvider(config ProviderConfig) *Provider
NewProvider builds a simulated provider from config, applying defaults for any unset field.
func (*Provider) InFlight ¶
InFlight reports how many sends currently hold a connection. It never exceeds the configured connection limit; surplus callers block in Send until a connection frees up.
func (*Provider) Send ¶
Send simulates delivering one email, honoring ctx cancellation throughout. It first claims one of the provider's limited connections — blocking if all are in use — then waits out the round-trip latency before deciding the outcome: an outage rejects every call, a hard-bounce address fails permanently, and an otherwise healthy send fails transiently at the configured rate.
func (*Provider) SetDown ¶
SetDown switches the provider into (down=true) or out of (down=false) a total outage. While down, every Send returns ErrProviderDown, which trips the per-task-type circuit breaker; clearing it lets the breaker recover.
func (*Provider) Stats ¶
func (p *Provider) Stats() ProviderStats
Stats snapshots the running send-outcome counters.
type ProviderConfig ¶
type ProviderConfig struct {
// MaxConnections is the concurrent-send limit; zero selects the default.
MaxConnections int
// Latency is the simulated per-send round trip; zero selects the default.
Latency time.Duration
// FailureRate is the transient-failure probability in [0,1] while healthy;
// the zero value never fails transiently. Use DefaultProviderConfig for the
// realistic default that exercises retries.
FailureRate float64
}
ProviderConfig configures a simulated provider. The zero value is usable: it applies the package defaults.
func DefaultProviderConfig ¶
func DefaultProviderConfig() ProviderConfig
DefaultProviderConfig returns the realistic provider settings the example's commands run with: a tight connection limit and a fraction of sends failing transiently, so retries with backoff are exercised. Tests build providers directly with a zero FailureRate when they need deterministic delivery.
type ProviderStats ¶
type ProviderStats struct {
// Sent is the number of successful deliveries.
Sent int64
// Transient is the number of transient failures (including outage rejections).
Transient int64
// Bounced is the number of permanent hard-bounce rejections.
Bounced int64
}
ProviderStats is a point-in-time snapshot of a provider's send outcomes.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
producer
command
Command producer simulates the customer apps hitting the platform's API: it enqueues a continuous, transactional-heavy mix of notification tasks against a conveyord node until interrupted.
|
Command producer simulates the customer apps hitting the platform's API: it enqueues a continuous, transactional-heavy mix of notification tasks against a conveyord node until interrupted. |
|
worker
command
Command worker is a Postmark worker process: it connects to a conveyord node, serves the platform's three queues, and delivers mail through the simulated provider until interrupted.
|
Command worker is a Postmark worker process: it connects to a conveyord node, serves the platform's three queues, and delivers mail through the simulated provider until interrupted. |