package mailstore import ( "context" "database/sql" "errors" "fmt" pgtable "galaxy/mail/internal/adapters/postgres/jet/mail/table" "galaxy/mail/internal/service/renderdelivery" pg "github.com/go-jet/jet/v2/postgres" ) // RenderDelivery returns a handle that satisfies renderdelivery.Store. func (store *Store) RenderDelivery() *RenderDeliveryStore { return &RenderDeliveryStore{store: store} } // RenderDeliveryStore is the renderdelivery.Store handle returned by // Store.RenderDelivery. type RenderDeliveryStore struct { store *Store } var _ renderdelivery.Store = (*RenderDeliveryStore)(nil) // MarkRendered persists the rendered subject, bodies, and locale_fallback // flag for a queued template-mode delivery and transitions its status to // rendered. The active attempt remains scheduled with its existing // scheduled_for so the scheduler picks the row up via next_attempt_at. func (handle *RenderDeliveryStore) MarkRendered(ctx context.Context, input renderdelivery.MarkRenderedInput) error { if handle == nil || handle.store == nil { return errors.New("mark rendered: nil store") } if ctx == nil { return errors.New("mark rendered: nil context") } if err := input.Validate(); err != nil { return fmt.Errorf("mark rendered: %w", err) } return handle.store.withTx(ctx, "mark rendered", func(ctx context.Context, tx *sql.Tx) error { // Lock the active attempt for the duration of the update so a // concurrent attempt-claim races against the same row. lockStmt := pg.SELECT(pgtable.Attempts.ScheduledFor). FROM(pgtable.Attempts). WHERE(pg.AND( pgtable.Attempts.DeliveryID.EQ(pg.String(input.Delivery.DeliveryID.String())), pgtable.Attempts.AttemptNo.EQ(pg.Int(int64(input.Delivery.AttemptCount))), )). FOR(pg.UPDATE()) lockQuery, lockArgs := lockStmt.Sql() row := tx.QueryRowContext(ctx, lockQuery, lockArgs...) var ignored any if err := row.Scan(&ignored); err != nil { return fmt.Errorf("mark rendered: lock active attempt: %w", err) } if err := lockDelivery(ctx, tx, input.Delivery.DeliveryID); err != nil { return fmt.Errorf("mark rendered: %w", err) } activeAttempt, err := loadActiveAttempt(ctx, tx, input.Delivery.DeliveryID, input.Delivery.AttemptCount) if err != nil { return fmt.Errorf("mark rendered: load active attempt: %w", err) } if err := updateDelivery(ctx, tx, input.Delivery, &activeAttempt); err != nil { return fmt.Errorf("mark rendered: update delivery: %w", err) } return nil }) } // MarkRenderFailed persists one classified terminal render failure. The // active attempt becomes terminal (`render_failed`) and the delivery becomes // `failed`. func (handle *RenderDeliveryStore) MarkRenderFailed(ctx context.Context, input renderdelivery.MarkRenderFailedInput) error { if handle == nil || handle.store == nil { return errors.New("mark render failed: nil store") } if ctx == nil { return errors.New("mark render failed: nil context") } if err := input.Validate(); err != nil { return fmt.Errorf("mark render failed: %w", err) } return handle.store.withTx(ctx, "mark render failed", func(ctx context.Context, tx *sql.Tx) error { if err := lockDelivery(ctx, tx, input.Delivery.DeliveryID); err != nil { return fmt.Errorf("mark render failed: %w", err) } if err := updateAttempt(ctx, tx, input.Attempt); err != nil { return fmt.Errorf("mark render failed: update attempt: %w", err) } if err := updateDelivery(ctx, tx, input.Delivery, nil); err != nil { return fmt.Errorf("mark render failed: update delivery: %w", err) } return nil }) }