Make new_suggestions and Send to Google Chat recoverable
MQ-167-01: a scheduled or manual batch now emits new_suggestions:{batchId}
in the transaction that first stores its Side quests, so the cards and
their notification commit together. A later round that fails, or a
failure recording the batch's end, no longer loses the event; a later
round's emit is a no-op by key; a notification that cannot be stored
fails that store, leaving no unannounced cards. Replenish batches still
never notify. finishBatch goes back to recording status only.
MQ-167-02: migration 0017 adds shopping_lists.chat_send_unconfirmed_at.
SendToChat records the send as begun before posting, clears it with
sent_to_chat_at once Google Chat accepts, and clears it alone when Chat
refuses. A post Chat does not answer, or one it accepts that cannot be
recorded, is ErrSendUnconfirmed (502 chat_unconfirmed, not retryable)
and leaves the marker, as does a crash mid-send; a repeat without resend
never reposts, and resend posts again. The Supply run screen re-reads
the list after a failed send, shows an unconfirmed send, offers no Retry
for it, and sends again only with resend.
Task: f0cd9d50-9c11-41be-a945-7bae9f5f28a4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+12
-24
@@ -257,39 +257,27 @@ func (s *Service) startBatch(ctx context.Context, tx *sql.Tx, trigger string) (B
|
||||
return Batch{ID: row.ID, JobID: jobID, Trigger: row.Trigger, Status: row.Status, Created: true}, nil
|
||||
}
|
||||
|
||||
// finishBatch records running batch id as status, and emits ev with it
|
||||
// when ev is not nil, with jobs.FinalWriteContext: the write is made even
|
||||
// when ctx has been cancelled, but a writer that stays busy cannot hold its
|
||||
// worker past jobs.FinalWriteTimeout. Its job's handler returns any error, so the job
|
||||
// finishBatch records running batch id as status, with
|
||||
// jobs.FinalWriteContext: the write is made even when ctx has been
|
||||
// cancelled, but a writer that stays busy cannot hold its worker past
|
||||
// jobs.FinalWriteTimeout. Its job's handler returns any error, so the job
|
||||
// never ends succeeded with the batch still running; the runner keeps only
|
||||
// a code, so the error is logged too.
|
||||
func (s *Service) finishBatch(ctx context.Context, id, status string, ev *notify.Event) error {
|
||||
func (s *Service) finishBatch(ctx context.Context, id, status string) error {
|
||||
write, cancel := jobs.FinalWriteContext(ctx)
|
||||
defer cancel()
|
||||
err := s.st.WithTx(write, func(tx *sql.Tx) error {
|
||||
n, err := db.New(tx).FinishSuggestionBatch(write, db.FinishSuggestionBatchParams{
|
||||
Status: status,
|
||||
UpdatedAt: store.Now().String(),
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
return errors.New("batch is not running")
|
||||
}
|
||||
if ev == nil {
|
||||
return nil
|
||||
}
|
||||
return s.notifier.EmitTx(write, tx, *ev)
|
||||
n, err := db.New(s.st.Writer).FinishSuggestionBatch(write, db.FinishSuggestionBatchParams{
|
||||
Status: status,
|
||||
UpdatedAt: store.Now().String(),
|
||||
ID: id,
|
||||
})
|
||||
if err == nil && n != 1 {
|
||||
err = errors.New("batch is not running")
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, "record suggestion batch status", "batch_id", id, "status", status, "error", err)
|
||||
return fmt.Errorf("suggest: record batch %s %s: %w", id, status, err)
|
||||
}
|
||||
if ev != nil {
|
||||
s.notifier.Wake()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user