Documentation
¶
Overview ¶
Package senderidentity manages the per-domain SES sending identity that lets outbound mail use the agent's OWN address as the From header (decision 4 / Slice 4). Verification is asynchronous: a domain moves none → pending → verified|failed, driven by a River-backed provision job + a periodic reconciler. The own-address From is used ONLY when the domain reaches `verified` (fail-closed); every other state falls back to the relay From, so the whole subsystem is behavior-neutral until a Provider actually verifies a domain.
The Provider abstraction keeps the AWS SES SDK at the edge: the workers, store, and handlers speak this interface, and tests use the in-memory fake. The real sesv2 implementation (ses.go) is exercised only against live AWS; everything else is unit/integration tested with the fake.
Index ¶
- Constants
- Variables
- type Config
- type DNSRecord
- type DeprovisionArgs
- type DeprovisionWorker
- type EventFirer
- type FakeProvider
- func (f *FakeProvider) Deprovision(ctx context.Context, domain string) error
- func (f *FakeProvider) List(ctx context.Context) ([]string, error)
- func (f *FakeProvider) ListPage(ctx context.Context, nextToken string, limit int) ([]string, string, error)
- func (f *FakeProvider) Provision(ctx context.Context, domain, dkimSelector string, dkimPrivateKeyDER []byte) (Result, error)
- func (f *FakeProvider) SeedIdentity(domain string)
- func (f *FakeProvider) SetDeprovisionErr(err error)
- func (f *FakeProvider) SetProvisionErr(err error)
- func (f *FakeProvider) SetProvisionResult(result Result)
- func (f *FakeProvider) SetStatus(domain string, r Result)
- func (f *FakeProvider) SetStatusErr(domain string, err error)
- func (f *FakeProvider) SetStatusNotFound(domain string)
- func (f *FakeProvider) SetStatusSequence(domain string, seq ...Result)
- func (f *FakeProvider) Status(ctx context.Context, domain string) (Result, error)
- type LegacyReapWorker
- type Manager
- func (m *Manager) EnqueueDeprovisionTx(ctx context.Context, tx pgx.Tx, domain string) error
- func (m *Manager) EnqueueProvision(ctx context.Context, domain string) error
- func (m *Manager) EnqueueProvisionTx(ctx context.Context, tx pgx.Tx, domain string) error
- func (m *Manager) RegisterJobs(w *river.Workers) []*river.PeriodicJob
- func (m *Manager) SetEnqueuer(e jobs.Enqueuer)
- func (m *Manager) TryDeprovisionNow(ctx context.Context, domain string) (confirmed bool, err error)
- type PostDrainAuditArgs
- type PostDrainAuditWorker
- type Provider
- type ProvisionArgs
- type ProvisionWorker
- type RawStore
- type ReapArgs
- type ReapV2Args
- type ReapWorker
- type ReconcileArgs
- type ReconcileV2Args
- type ReconcileV2Worker
- type ReconcileWorker
- type Result
- type SESProvider
- func (p *SESProvider) Deprovision(ctx context.Context, domain string) error
- func (p *SESProvider) List(ctx context.Context) ([]string, error)
- func (p *SESProvider) ListPage(ctx context.Context, nextToken string, limit int) ([]string, string, error)
- func (p *SESProvider) Provision(ctx context.Context, domain, dkimSelector string, dkimPrivateKeyDER []byte) (Result, error)
- func (p *SESProvider) Status(ctx context.Context, domain string) (Result, error)
- type SendingIdentityState
- type Status
- type Store
- type SyncArgs
- type SyncWorker
Constants ¶
const DefaultMaxReconcileAttempts = 12
DefaultMaxReconcileAttempts bounds how long a domain may sit in `pending` before the reconciler gives up and marks it `failed` (the design's "no infinite poll" TTL). The wall-clock TTL is the sum of River's retry backoffs across this many attempts.
Variables ¶
var ErrIdentityNotFound = errors.New("senderidentity: identity not found")
ErrIdentityNotFound is what Status returns when the provider has no identity for the domain (e.g. it was deprovisioned out of band). Callers treat it as "drop back to none/failed", never as a hard error.
var ErrIdentityNotOwned = errors.New("senderidentity: identity is not managed by e2a")
ErrIdentityNotOwned means an identity exists for the domain but lacks the provider-side ownership marker written by e2a. Callers must never update or delete it: an SES account can be shared with other applications.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// MaxReconcileAttempts bounds the pending→failed TTL. 0 → default.
MaxReconcileAttempts int
// ReaperInterval overrides the orphan-sweep cadence. 0 → default.
ReaperInterval time.Duration
// LegacyJobCompat is phase 1 of the two-phase blue/green job-lane rollout
// (config sender_identity.legacy_job_compat).
// When true, mutation and reconcile jobs are PRODUCED as the legacy kinds
// on the default queue — consumable by the previous release — while this
// binary still CONSUMES both lanes. Deploy this release with the flag on;
// once it is the stable rollback target, flip the flag off (a config-only
// deploy) to switch producers to the versioned v2 lane. A rollback of
// that second deploy lands on a binary that consumes v2, so nothing
// strands. This only makes the River lanes rollback-compatible: operators
// must still freeze sender-identity mutations while a pre-ownership worker
// overlaps or is a possible rollback target (see the deployment design).
// Default false: single-instance/self-host deployments have no blue/green
// overlap and want the v2 semantics immediately.
LegacyJobCompat bool
}
Config tunes the Manager. Zero values get sane defaults.
type DNSRecord ¶
type DNSRecord struct {
Type string `json:"type"` // "TXT" | "CNAME" | "MX"
Name string `json:"name"` // record host
Value string `json:"value"` // record value
}
DNSRecord is a single record the customer must publish for the sending identity. With BYODKIM the customer already published the per-domain DKIM record during register/verify; this now carries the custom MAIL FROM subdomain's MX + SPF records (Return-Path alignment — see ses.go mailFromRecords) and surfaces anything SES reports as still-required.
type DeprovisionArgs ¶
type DeprovisionArgs struct {
Domain string `json:"domain"`
}
func (DeprovisionArgs) Kind ¶
func (DeprovisionArgs) Kind() string
type DeprovisionWorker ¶
type DeprovisionWorker struct {
river.WorkerDefaults[DeprovisionArgs]
// contains filtered or unexported fields
}
DeprovisionWorker removes the SES sending identity on domain/account delete. Idempotent: the provider treats a missing identity as success.
func (*DeprovisionWorker) Work ¶
func (w *DeprovisionWorker) Work(ctx context.Context, job *river.Job[DeprovisionArgs]) error
type EventFirer ¶
EventFirer publishes a domain.sending_verified / domain.sending_failed event. Injected as a closure so this package doesn't depend on webhookpub. userID is the domain owner; a nil firer (tests) is a no-op.
type FakeProvider ¶
type FakeProvider struct {
ProvisionCalls []string
StatusCalls []string
DeprovisionCalls []string
ListCalls int
ListPageCalls int
// contains filtered or unexported fields
}
FakeProvider is an in-memory Provider for tests. It is concurrency-safe (the workers call it from River goroutines). Configure per-domain behavior with the setters; inspect calls with the recorded slices.
Default behavior with no configuration: Provision returns StatusPending, Status returns StatusPending forever, Deprovision succeeds. Tests that want a domain to verify call SetStatusSequence or SetStatus.
func NewFakeProvider ¶
func NewFakeProvider() *FakeProvider
NewFakeProvider returns a ready FakeProvider with default behavior.
func (*FakeProvider) Deprovision ¶
func (f *FakeProvider) Deprovision(ctx context.Context, domain string) error
func (*FakeProvider) SeedIdentity ¶
func (f *FakeProvider) SeedIdentity(domain string)
SeedIdentity marks domain as having a provider identity (for List/reaper tests) without going through Provision.
func (*FakeProvider) SetDeprovisionErr ¶
func (f *FakeProvider) SetDeprovisionErr(err error)
SetDeprovisionErr forces Deprovision to fail.
func (*FakeProvider) SetProvisionErr ¶
func (f *FakeProvider) SetProvisionErr(err error)
SetProvisionErr makes the next Provision calls return err.
func (*FakeProvider) SetProvisionResult ¶ added in v1.7.8
func (f *FakeProvider) SetProvisionResult(result Result)
SetProvisionResult overrides the default pending provisioning result.
func (*FakeProvider) SetStatus ¶
func (f *FakeProvider) SetStatus(domain string, r Result)
SetStatus fixes the Result returned by Status for domain.
func (*FakeProvider) SetStatusErr ¶
func (f *FakeProvider) SetStatusErr(domain string, err error)
SetStatusErr makes Status return err (a transient error, not NotFound) for domain.
func (*FakeProvider) SetStatusNotFound ¶
func (f *FakeProvider) SetStatusNotFound(domain string)
SetStatusNotFound makes Status return ErrIdentityNotFound for domain.
func (*FakeProvider) SetStatusSequence ¶
func (f *FakeProvider) SetStatusSequence(domain string, seq ...Result)
SetStatusSequence queues results consumed one-per-Status-call for domain; the last result repeats once the sequence drains. Lets a test drive pending → pending → verified.
type LegacyReapWorker ¶ added in v1.7.8
type LegacyReapWorker struct {
river.WorkerDefaults[ReapArgs]
// contains filtered or unexported fields
}
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the sender-identity job lifecycle on the SHARED River client (internal/jobs), instead of a private client. It is a jobs.Registrar — it contributes rollout-compatible legacy workers, v2 sync/reconcile workers, and the periodic managed-identity reaper — plus the app's enqueue entry point: EnqueueProvision on domain verify, EnqueueDeprovisionTx in the domain-delete tx. The shared client is injected via SetEnqueuer after jobs.New has built it (which needs this Manager as a Registrar first — the standard two-phase wiring).
func NewManager ¶
func NewManager(store Store, provider Provider, fire EventFirer, cfg Config) *Manager
NewManager builds the manager with its dependencies. It does NOT build a River client — call jobs.New with this Manager as a Registrar, then SetEnqueuer with the resulting client. fire may be nil (no events).
func (*Manager) EnqueueDeprovisionTx ¶
EnqueueDeprovisionTx enqueues sending-identity teardown WITHIN the caller's delete transaction, so the job is committed atomically with the domain-row delete — it can never be lost if SES is unreachable at delete time.
func (*Manager) EnqueueProvision ¶
EnqueueProvision schedules sending-identity provisioning for a domain (called when a domain becomes verified, or on a forced re-check via POST /domains/{domain}/verify).
Intentionally NOT unique: River's job uniqueness can't drop `completed` from its state set (only `retryable` is safely removable), so a completed job would block a legitimate re-provision — e.g. POST /verify retrying a `failed` domain — for the ~24h completed-job retention window. (Where dedupe IS wanted despite that constraint, scope uniqueness in time instead — see PostDrainAuditArgs.Window.) Instead we always enqueue and rely on desired-state convergence: the current incarnation's BYODKIM + MAIL FROM replaces any prior provider state. Provider mutations are serialized per process and per domain across replicas, so concurrent duplicate enqueues are harmless.
func (*Manager) EnqueueProvisionTx ¶ added in v1.7.8
EnqueueProvisionTx is the verify-time atomic-outbox variant.
func (*Manager) RegisterJobs ¶
func (m *Manager) RegisterJobs(w *river.Workers) []*river.PeriodicJob
RegisterJobs adds both legacy drain workers and rollout-safe v2 workers to the shared client, and returns the v2 periodic reaper. Implements jobs.Registrar. The workers run on the default queue (nil InsertOpts.Queue), preserving prior behavior.
func (*Manager) SetEnqueuer ¶
SetEnqueuer injects the shared client so the Enqueue* methods can insert jobs. Must be called (once, at startup) before EnqueueProvision/EnqueueDeprovisionTx.
func (*Manager) TryDeprovisionNow ¶ added in v1.7.8
TryDeprovisionNow is the post-commit best-effort convergence attempt for a just-deleted domain: the delete transaction has already committed the row delete plus the durable teardown job, so the provider identity is usually confirmed absent before the HTTP response returns — but a failure here is for the caller to LOG, never to propagate to the client (the committed job and the hourly reaper are the guarantee). It runs the same desired-state convergence as the workers: the absent row converges to provider absence, ErrIdentityNotOwned is tolerated (a foreign identity is not e2a's to delete) but returned as confirmed=false so callers never claim provider absence or release DNS. The ledger tombstone is deliberately retained for the post-drain audit to finalize (mixed-version late-create repair). The caller bounds the wait via ctx; the mutation gate honors cancellation.
type PostDrainAuditArgs ¶ added in v1.7.8
type PostDrainAuditArgs struct {
Domain string `json:"domain" river:"unique"`
// Window is the audit's schedule bucket (scheduled-at unix seconds /
// postDrainConvergenceDelay). It scopes ByArgs uniqueness in time: River's
// unique state set cannot drop `completed`, so without it a completed
// audit inside the ~24h retention blocked the next mutation's audit for
// the same domain. A completed audit's bucket is strictly older than any
// new mutation's (an audit for bucket B cannot complete before B's start,
// and any later mutation schedules into a later bucket — modulo replica
// clock skew, which shrinks that guarantee by the skew), so dedupe can
// only ever match a still-scheduled job. Mutations straddling a bucket
// edge produce two audits; the audit is idempotent, so that is noise,
// not a bug.
Window int64 `json:"window" river:"unique"`
}
PostDrainAuditArgs is a domain-scoped, deduplicated finalizer scheduled by mutations that can overlap a legacy blue/green slot. It runs only on the v2 queue, nominally postDrainConvergenceDelay after its mutation — but see Window: a mutation deduped into an audit scheduled earlier in the same bucket can be finalized up to one bucket early, i.e. possibly while the old slot still drains. The hourly reaper and the orphan ALERT remain the backstop for that residual window (which the pre-Window code had in an unbounded form).
func (PostDrainAuditArgs) Kind ¶ added in v1.7.8
func (PostDrainAuditArgs) Kind() string
type PostDrainAuditWorker ¶ added in v1.7.8
type PostDrainAuditWorker struct {
river.WorkerDefaults[PostDrainAuditArgs]
// contains filtered or unexported fields
}
func (*PostDrainAuditWorker) Work ¶ added in v1.7.8
func (w *PostDrainAuditWorker) Work(ctx context.Context, job *river.Job[PostDrainAuditArgs]) error
type Provider ¶
type Provider interface {
// Provision registers a BYODKIM sending identity for domain, supplying
// the per-domain DKIM selector + PKCS#1 DER private key that e2a
// already generated (so DKIM d= aligns with the From domain). Returns
// the initial Result — typically StatusPending — or an error to retry.
Provision(ctx context.Context, domain, dkimSelector string, dkimPrivateKeyDER []byte) (Result, error)
// Status polls the current verification state from the provider.
// Returns ErrIdentityNotFound if no identity exists for domain.
Status(ctx context.Context, domain string) (Result, error)
// Deprovision removes the sending identity. A missing identity MUST be
// reported as success (idempotent teardown).
Deprovision(ctx context.Context, domain string) error
// List returns all domain identities visible to the provider principal.
// It is retained for the phase-1 compatibility worker only; the v2 reaper
// uses ListPage so one River job cannot inventory the whole provider account.
// Neither path treats an unledgered identity as its own.
List(ctx context.Context) ([]string, error)
// ListPage returns one provider-bounded page plus the opaque continuation
// token. The v2 orphan audit uses this so one River job never inventories
// the whole provider account.
ListPage(ctx context.Context, nextToken string, limit int) (domains []string, followingToken string, err error)
}
Provider registers, polls, and removes the upstream (SES) sending identity for a domain. Implementations MUST be idempotent for identities they own: Provision on an already-managed domain refreshes desired state, and Deprovision treats a missing identity as success. An existing identity that lacks the provider ownership marker returns ErrIdentityNotOwned and must not be mutated.
type ProvisionArgs ¶
type ProvisionArgs struct {
Domain string `json:"domain"`
}
func (ProvisionArgs) Kind ¶
func (ProvisionArgs) Kind() string
type ProvisionWorker ¶
type ProvisionWorker struct {
river.WorkerDefaults[ProvisionArgs]
// contains filtered or unexported fields
}
ProvisionWorker registers the SES sending identity (BYODKIM) for a domain and, on success, enqueues a reconcile job to poll it to verified.
func (*ProvisionWorker) Work ¶
func (w *ProvisionWorker) Work(ctx context.Context, job *river.Job[ProvisionArgs]) error
type RawStore ¶
type RawStore interface {
WithSendingIdentityMutationLock(ctx context.Context, domain string, fn func(context.Context) error) error
LoadSendingIdentityState(ctx context.Context, domain string) (incarnation, owner string, verified bool, status, selector string, privateKeyDER []byte, appliedIncarnation string, ledgerUpdatedAt time.Time, err error)
SetSendingStatusForIncarnation(ctx context.Context, domain, incarnation, status, dkimStatus, mailFromStatus, errMsg string, recordsJSON []byte) error
TouchSendingCheckedForIncarnation(ctx context.Context, domain, incarnation string) error
MarkSendingIdentityManaged(ctx context.Context, domain, incarnation string) error
MarkSendingIdentityApplied(ctx context.Context, domain, incarnation string) error
SendingIdentityLedgerExpired(ctx context.Context, domain, incarnation string, olderThan time.Duration) (bool, error)
ObserveSendingIdentityProviderPending(ctx context.Context, domain, incarnation string, olderThan time.Duration) (bool, error)
ClearSendingIdentityProviderPending(ctx context.Context, domain, incarnation string) error
ForgetSendingIdentityManaged(ctx context.Context, domain string) error
FinalizeSendingIdentityTombstone(ctx context.Context, domain string, olderThan time.Duration) error
SetDomainTeardownState(ctx context.Context, domain string, state domainteardown.State) error
ListManagedSendingIdentityDomains(ctx context.Context) ([]string, map[string]bool, error)
ListManagedSendingIdentityDomainsPage(ctx context.Context, afterDomain string, limit int) ([]string, map[string]bool, bool, error)
LookupManagedSendingIdentityDomain(ctx context.Context, domain string) (bool, bool, error)
DomainExists(ctx context.Context, domain string) (bool, error)
}
RawStore is the primitive persistence surface implemented by *identity.Store. It deliberately speaks plain strings / JSON bytes so the core identity package does NOT import senderidentity (and thus does not pull River + the AWS SDK into its dependency graph). NewStoreAdapter wraps it into the typed Store the workers consume.
type ReapArgs ¶
type ReapArgs struct{}
ReapArgs is the legacy periodic kind. It stays registered so jobs inserted by the draining release are handled safely by the new worker.
type ReapV2Args ¶ added in v1.7.8
type ReapV2Args struct {
SweepID int64 `json:"sweep_id,omitempty" river:"unique"`
AfterDomain string `json:"after_domain,omitempty" river:"unique"`
Phase string `json:"phase,omitempty" river:"unique"`
ProviderToken string `json:"provider_token,omitempty" river:"unique"`
}
ReapV2Args prevents the old blue/green slot from claiming new convergence sweeps during rollout.
func (ReapV2Args) Kind ¶ added in v1.7.8
func (ReapV2Args) Kind() string
type ReapWorker ¶
type ReapWorker struct {
river.WorkerDefaults[ReapV2Args]
// contains filtered or unexported fields
}
ReapWorker is the durable teardown/provisioning backstop. Normal mutation jobs retry promptly, while this hourly sweep keeps retrying the bounded managed-domain ledger after River exhausts a job's finite attempt budget. It never deletes arbitrary identities returned by SES List: only domains in e2a's durable ownership ledger are eligible.
func (*ReapWorker) Work ¶
func (w *ReapWorker) Work(ctx context.Context, job *river.Job[ReapV2Args]) error
type ReconcileArgs ¶
type ReconcileArgs struct {
Domain string `json:"domain"`
Incarnation string `json:"incarnation,omitempty"`
}
func (ReconcileArgs) Kind ¶
func (ReconcileArgs) Kind() string
type ReconcileV2Args ¶ added in v1.7.8
type ReconcileV2Args struct {
Domain string `json:"domain"`
Incarnation string `json:"incarnation"`
}
ReconcileV2Args likewise keeps new incarnation-aware polls away from an old worker that would ignore the incarnation field during a blue/green overlap.
func (ReconcileV2Args) Kind ¶ added in v1.7.8
func (ReconcileV2Args) Kind() string
type ReconcileV2Worker ¶ added in v1.7.8
type ReconcileV2Worker struct {
river.WorkerDefaults[ReconcileV2Args]
// contains filtered or unexported fields
}
func (*ReconcileV2Worker) Work ¶ added in v1.7.8
func (w *ReconcileV2Worker) Work(ctx context.Context, job *river.Job[ReconcileV2Args]) error
type ReconcileWorker ¶
type ReconcileWorker struct {
river.WorkerDefaults[ReconcileArgs]
// contains filtered or unexported fields
}
ReconcileWorker polls SES for a pending domain and transitions it to verified/failed. While still pending it returns errStillPending so River retries with backoff; once the attempt budget is exhausted it marks the domain failed (bounded TTL — no infinite poll).
func (*ReconcileWorker) Work ¶
func (w *ReconcileWorker) Work(ctx context.Context, job *river.Job[ReconcileArgs]) error
type Result ¶
type Result struct {
Status Status `json:"status"`
DkimStatus Status `json:"dkim_status,omitempty"`
MailFromStatus Status `json:"mail_from_status,omitempty"`
Error string `json:"error,omitempty"`
DNSRecords []DNSRecord `json:"dns_records,omitempty"`
}
Result is what a Provider reports for a domain.
Status is the all-or-nothing rollup (mapSESStatus): `verified` only when EVERY sending axis is good. DkimStatus and MailFromStatus are the per-axis breakdown SES reports independently (DkimAttributes.Status and MailFromAttributes.MailFromDomainStatus), so a domain with good DKIM but a broken custom MAIL FROM surfaces as DkimStatus=verified + MailFromStatus=failed while the rollup Status stays `failed`. They are empty ("") when the provider has no per-axis signal (e.g. Provision, which only registers the identity); consumers fall back to the rollup in that case.
type SESProvider ¶
type SESProvider struct {
// contains filtered or unexported fields
}
SESProvider is the real Provider backed by AWS SES v2. It registers domain sending identities with BYODKIM, reusing e2a's per-domain DKIM key so the DKIM d= aligns with the From domain (DMARC passes on DKIM alignment), and configures a custom MAIL FROM subdomain (bounce.<domain>) so the Return-Path aligns too (SPF passes on the From org-domain → no "via e2a"). Every created identity carries an e2a ownership tag. Existing untagged identities are never adopted or mutated; IAM independently applies the same resource-tag condition to close the client-side check/mutation race.
func NewSESProvider ¶
func NewSESProvider(api sesAPI, region string) *SESProvider
NewSESProvider wraps a pre-built SES API (or stub). region feeds the MAIL FROM MX record target.
func NewSESProviderFromConfig ¶
func NewSESProviderFromConfig(ctx context.Context, region string) (*SESProvider, error)
NewSESProviderFromConfig builds a provider from ambient AWS config (env/instance role) for the given region.
func (*SESProvider) Deprovision ¶
func (p *SESProvider) Deprovision(ctx context.Context, domain string) error
type SendingIdentityState ¶ added in v1.7.8
type SendingIdentityState struct {
Incarnation string
Owner string
Verified bool
Status Status
Selector string
PrivateKey []byte
// AppliedIncarnation is the ledger's provider-confirmed incarnation ("" if
// none). Equal to Incarnation only when THIS registration's key was
// confirmed installed — the gate for the healthy-recheck no-op.
AppliedIncarnation string
// LedgerUpdatedAt remains available for diagnostics and adapters. Timeout
// decisions use Store.SendingIdentityLedgerExpired so clocks are not mixed.
LedgerUpdatedAt time.Time
}
SendingIdentityState is the incarnation-consistent desired-state snapshot that both provision and deprovision jobs converge at execution time.
type Status ¶
type Status string
Status is the verification state of a domain's sending identity. It maps 1:1 onto the domains.sending_status column.
const ( // StatusNone — no sending identity registered. Default for every // domain; self-host / SES-not-configured deployments stay here, which // keeps outbound on the relay From (fail-closed). StatusNone Status = "none" // StatusPending — identity registered with SES (BYODKIM), awaiting // asynchronous verification. The reconciler polls until it resolves. StatusPending Status = "pending" // StatusVerified — SES confirmed the identity; own-address From is now // used for this domain's agents. StatusVerified Status = "verified" // StatusFailed — verification failed, or `pending` exceeded its TTL. // Carries an actionable reason; outbound stays on the relay From. StatusFailed Status = "failed" )
type Store ¶
type Store interface {
// WithSendingIdentityMutationLock serializes provider create/delete calls
// for one domain across processes and passes a context pinned to the lock.
WithSendingIdentityMutationLock(ctx context.Context, domain string, fn func(context.Context) error) error
// LoadSendingIdentityState returns the current domain incarnation and its
// desired provider state. pgx.ErrNoRows means the desired state is absent.
LoadSendingIdentityState(ctx context.Context, domain string) (SendingIdentityState, error)
// SetSendingStatus writes a terminal/transition status (+ the per-axis
// dkim/mailFrom breakdown + error + DNS records) and stamps
// sending_last_checked_at. dkimStatus/mailFromStatus may be empty ("")
// when the caller has no per-axis signal (e.g. provision, or a terminal
// failure with no SES poll); persisting empty lets the read path fall back
// to the all-or-nothing rollup.
SetSendingStatus(ctx context.Context, domain, incarnation string, status, dkimStatus, mailFromStatus Status, errMsg string, records []DNSRecord) error
// TouchSendingChecked stamps sending_last_checked_at without changing the
// status — used on a still-pending poll.
TouchSendingChecked(ctx context.Context, domain, incarnation string) error
// The managed-domain ledger survives domain deletion so exhausted River
// jobs remain repairable without scanning/deleting unrelated identities in
// the provider account.
MarkSendingIdentityManaged(ctx context.Context, domain, incarnation string) error
MarkSendingIdentityApplied(ctx context.Context, domain, incarnation string) error
// SendingIdentityLedgerExpired compares entirely on the database clock so
// application/DB clock skew cannot shorten or extend the pending TTL.
SendingIdentityLedgerExpired(ctx context.Context, domain, incarnation string, olderThan time.Duration) (bool, error)
// ObserveSendingIdentityProviderPending persists the first provider-pending
// observation for a DB-verified identity and reports whether its grace has
// expired, also using the database clock. Clear removes that drift marker
// once the provider agrees or a terminal transition is committed.
ObserveSendingIdentityProviderPending(ctx context.Context, domain, incarnation string, olderThan time.Duration) (bool, error)
ClearSendingIdentityProviderPending(ctx context.Context, domain, incarnation string) error
ForgetSendingIdentityManaged(ctx context.Context, domain string) error
// FinalizeSendingIdentityTombstone removes the ledger row ONLY when its
// last mutation (updated_at) is older than olderThan. An audit or sweep
// that runs inside a later mutation's drain window therefore cannot
// finalize that mutation's tombstone out from under a still-draining
// legacy slot; the reaper (or that mutation's own audit) finalizes once
// the window has truly elapsed.
FinalizeSendingIdentityTombstone(ctx context.Context, domain string, olderThan time.Duration) error
SetDomainTeardownState(ctx context.Context, domain string, state domainteardown.State) error
ListManagedSendingIdentityDomains(ctx context.Context) ([]string, map[string]bool, error)
ListManagedSendingIdentityDomainsPage(ctx context.Context, afterDomain string, limit int) ([]string, map[string]bool, bool, error)
LookupManagedSendingIdentityDomain(ctx context.Context, domain string) (needsProvision, found bool, err error)
// DomainExists reports whether a live domain row exists. The reaper uses
// it to ALERT on provider identities that are neither ledgered nor backed
// by a row (pre-upgrade orphans the migration backfill could not see).
DomainExists(ctx context.Context, domain string) (bool, error)
}
Store is the narrow persistence surface the workers need. *identity.Store satisfies it. Kept minimal so the workers don't depend on the whole store.
func NewStoreAdapter ¶
NewStoreAdapter bridges a RawStore (e.g. *identity.Store) to the typed Store the workers use, converting Status ↔ string and DNSRecord ↔ JSON.
type SyncArgs ¶ added in v1.7.8
type SyncArgs struct {
Domain string `json:"domain"`
}
SyncArgs is the rollout-safe desired-state mutation kind. Old blue/green binaries do not register this kind, so they cannot claim newly enqueued create/delete work while the new binary is baking or the old slot drains.
type SyncWorker ¶ added in v1.7.8
type SyncWorker struct {
river.WorkerDefaults[SyncArgs]
// contains filtered or unexported fields
}
SyncWorker handles all newly enqueued provider mutations. Legacy workers remain registered only to drain jobs written by the prior release.