ExpireApproveAndSend is the worker-side counterpart to ApproveAndSend: no user ownership check (the caller is the expiration worker, which is system-scoped), SELECT ... FOR NO KEY UPDATE SKIP LOCKED so concurrent workers don't race for the same row, and the terminal status is 'review_expired_approve
( ctx context.Context, messageID string, send func(msg *Message) (SendResult, error), )
| 2044 | // ErrNotPendingApproval — skip silently and let the next poll handle |
| 2045 | // it. |
| 2046 | func (s *Store) ExpireApproveAndSend( |
| 2047 | ctx context.Context, |
| 2048 | messageID string, |
| 2049 | send func(msg *Message) (SendResult, error), |
| 2050 | ) (*Message, error) { |
| 2051 | txCtx, cancel := context.WithTimeout(ctx, approvalTxTimeout) |
| 2052 | defer cancel() |
| 2053 | |
| 2054 | tx, err := s.pool.Begin(txCtx) |
| 2055 | if err != nil { |
| 2056 | return nil, err |
| 2057 | } |
| 2058 | committed := false |
| 2059 | defer func() { |
| 2060 | if !committed { |
| 2061 | _ = tx.Rollback(txCtx) |
| 2062 | } |
| 2063 | }() |
| 2064 | |
| 2065 | var ( |
| 2066 | m Message |
| 2067 | bodyText, bodyHTML *string |
| 2068 | attachments []byte |
| 2069 | method, msgType *string |
| 2070 | approvalExpires *time.Time |
| 2071 | ) |
| 2072 | err = tx.QueryRow(txCtx, |
| 2073 | `SELECT id, agent_id, direction, sender, recipient, subject, |
| 2074 | email_message_id, |
| 2075 | method, message_type, |
| 2076 | conversation_id, created_at, expires_at, |
| 2077 | to_recipients, cc, bcc, |
| 2078 | status, approval_expires_at, edited, |
| 2079 | body_text, body_html, attachments_json |
| 2080 | FROM messages |
| 2081 | WHERE id = $1 |
| 2082 | AND direction = 'outbound' |
| 2083 | AND status = 'pending_review' |
| 2084 | AND approval_expires_at < now() |
| 2085 | FOR NO KEY UPDATE SKIP LOCKED`, |
| 2086 | messageID, |
| 2087 | ).Scan( |
| 2088 | &m.ID, &m.AgentID, &m.Direction, &m.Sender, &m.Recipient, &m.Subject, |
| 2089 | &m.EmailMessageID, |
| 2090 | &method, &msgType, |
| 2091 | &m.ConversationID, &m.CreatedAt, &m.ExpiresAt, |
| 2092 | &m.ToRecipients, &m.CC, &m.BCC, |
| 2093 | &m.Status, &approvalExpires, &m.Edited, |
| 2094 | &bodyText, &bodyHTML, &attachments, |
| 2095 | ) |
| 2096 | if err != nil { |
| 2097 | // Row is either gone, no longer pending, not yet expired, or is |
| 2098 | // currently locked by another worker. Any of those means "someone |
| 2099 | // else will handle it, or nothing to do" — don't bubble as an error. |
| 2100 | return nil, ErrNotPendingApproval |
| 2101 | } |
| 2102 | if method != nil { |
| 2103 | m.Method = *method |