Feature 13 integration fix: deck error surfacing, active-batch tracking, modal key scope, three-card deck (#111)
Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
This commit was merged in pull request #111.
This commit is contained in:
+66
-35
@@ -171,34 +171,47 @@ func (s *Service) StartBatch(ctx context.Context, trigger string) (Batch, error)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// startBatch does StartBatch's work in tx: the batch and the job that runs
|
||||
// it are committed together, so a batch is never seen without its job. A
|
||||
// batch still pending or running whose job is no longer queued or running
|
||||
// is failed first, so a batch whose job ended without recording its end
|
||||
// (the process stopped, or the write failed) never blocks a later one. The
|
||||
// caller wakes the job runner once tx has committed and Created is true.
|
||||
func (s *Service) startBatch(ctx context.Context, tx *sql.Tx, trigger string) (Batch, error) {
|
||||
q := db.New(tx)
|
||||
// activeBatch returns the batch pending or running in q's transaction, or
|
||||
// nil when there is none. A batch still pending or running whose job is no
|
||||
// longer queued or running is failed first, so a batch whose job ended
|
||||
// without recording its end (the process stopped, or the write failed) is
|
||||
// never reported as active and never blocks a later one.
|
||||
func (s *Service) activeBatch(ctx context.Context, q *db.Queries) (*Batch, error) {
|
||||
n, err := q.FailOrphanedSuggestionBatches(ctx, db.FailOrphanedSuggestionBatchesParams{
|
||||
UpdatedAt: store.Now().String(),
|
||||
Kind: KindSuggestionBatch,
|
||||
})
|
||||
if err != nil {
|
||||
return Batch{}, fmt.Errorf("suggest: fail orphaned batches: %w", err)
|
||||
return nil, fmt.Errorf("suggest: fail orphaned batches: %w", err)
|
||||
}
|
||||
if n > 0 {
|
||||
s.logger.WarnContext(ctx, "failed suggestion batches whose job ended", "count", n)
|
||||
}
|
||||
cur, err := q.GetActiveSuggestionBatch(ctx)
|
||||
if err == nil {
|
||||
jobID, err := q.GetSuggestionBatchJobID(ctx, db.GetSuggestionBatchJobIDParams{Kind: KindSuggestionBatch, BatchID: cur.ID})
|
||||
if err != nil {
|
||||
return Batch{}, fmt.Errorf("suggest: read job of batch %s: %w", cur.ID, err)
|
||||
}
|
||||
return Batch{ID: cur.ID, JobID: jobID, Trigger: cur.Trigger, Status: cur.Status}, nil
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return Batch{}, fmt.Errorf("suggest: read active batch: %w", err)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("suggest: read active batch: %w", err)
|
||||
}
|
||||
jobID, err := q.GetSuggestionBatchJobID(ctx, db.GetSuggestionBatchJobIDParams{Kind: KindSuggestionBatch, BatchID: cur.ID})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("suggest: read job of batch %s: %w", cur.ID, err)
|
||||
}
|
||||
return &Batch{ID: cur.ID, JobID: jobID, Trigger: cur.Trigger, Status: cur.Status}, nil
|
||||
}
|
||||
|
||||
// startBatch does StartBatch's work in tx: the batch and the job that runs
|
||||
// it are committed together, so a batch is never seen without its job. The
|
||||
// caller wakes the job runner once tx has committed and Created is true.
|
||||
func (s *Service) startBatch(ctx context.Context, tx *sql.Tx, trigger string) (Batch, error) {
|
||||
q := db.New(tx)
|
||||
cur, err := s.activeBatch(ctx, q)
|
||||
if err != nil {
|
||||
return Batch{}, err
|
||||
}
|
||||
if cur != nil {
|
||||
return *cur, nil
|
||||
}
|
||||
now := store.Now().String()
|
||||
row, err := q.CreateSuggestionBatch(ctx, db.CreateSuggestionBatchParams{
|
||||
@@ -261,25 +274,44 @@ type Suggestion struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// Deck returns the pending suggestions whose dinner is still suggested,
|
||||
// oldest first.
|
||||
func (s *Service) Deck(ctx context.Context) ([]Suggestion, error) {
|
||||
rows, err := db.New(s.st.Reader).ListDeck(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("suggest: list deck: %w", err)
|
||||
}
|
||||
out := make([]Suggestion, len(rows))
|
||||
for i, row := range rows {
|
||||
out[i], err = fromRow(row.Suggestion, row.Dinner)
|
||||
// Deck is the household's deck: the pending suggestions whose dinner is
|
||||
// still suggested, oldest first, and the batch pending or running, if any,
|
||||
// so a client can follow the job producing more.
|
||||
type Deck struct {
|
||||
Suggestions []Suggestion `json:"suggestions"`
|
||||
ActiveBatch *Batch `json:"active_batch"`
|
||||
}
|
||||
|
||||
// Deck reads the deck and its active batch in one transaction, so both come
|
||||
// from the same snapshot. It is a write transaction because reading the
|
||||
// active batch first fails any batch whose job has ended (see activeBatch).
|
||||
func (s *Service) Deck(ctx context.Context) (Deck, error) {
|
||||
var deck Deck
|
||||
err := s.st.WithTx(ctx, func(tx *sql.Tx) error {
|
||||
q := db.New(tx)
|
||||
rows, err := q.ListDeck(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return fmt.Errorf("suggest: list deck: %w", err)
|
||||
}
|
||||
deck.Suggestions = make([]Suggestion, len(rows))
|
||||
for i, row := range rows {
|
||||
deck.Suggestions[i], err = fromRow(row.Suggestion, row.Dinner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
deck.ActiveBatch, err = s.activeBatch(ctx, q)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return Deck{}, err
|
||||
}
|
||||
return out, nil
|
||||
return deck, nil
|
||||
}
|
||||
|
||||
// Decision is a suggestion once decided, with the id of the replenish
|
||||
// batch's job when the decision started one.
|
||||
// batch's job when the decision called for one: the batch it started, or
|
||||
// the one already pending or running that it joined.
|
||||
type Decision struct {
|
||||
Suggestion Suggestion `json:"suggestion"`
|
||||
ReplenishJobID *string `json:"replenish_job_id"`
|
||||
@@ -303,8 +335,8 @@ func (s *Service) Decline(ctx context.Context, id, updatedBy string) (Decision,
|
||||
// dinnerStatus. Repeating a decision changes nothing and returns the
|
||||
// suggestion as it is; the opposite decision is a *DecidedError. A
|
||||
// decision that leaves the deck under ReplenishBelow starts a replenish
|
||||
// batch unless one is already pending or running, in the decision's own
|
||||
// transaction: the decision is never recorded without the replenish it
|
||||
// batch, or joins the one already pending or running, in the decision's own
|
||||
// transaction, and returns its job id either way: the decision is never recorded without the replenish it
|
||||
// calls for, and a failure to start the batch records neither.
|
||||
func (s *Service) decide(ctx context.Context, id, decision, dinnerStatus, updatedBy string) (Decision, error) {
|
||||
if updatedBy == "" {
|
||||
@@ -369,8 +401,7 @@ func (s *Service) decide(ctx context.Context, id, decision, dinnerStatus, update
|
||||
if err != nil {
|
||||
return Decision{}, err
|
||||
}
|
||||
created := replenish != nil && replenish.Created
|
||||
if created {
|
||||
if replenish != nil && replenish.Created {
|
||||
s.batchJob.Wake()
|
||||
}
|
||||
sug, err := fromRow(row.Suggestion, row.Dinner)
|
||||
@@ -378,7 +409,7 @@ func (s *Service) decide(ctx context.Context, id, decision, dinnerStatus, update
|
||||
return Decision{}, err
|
||||
}
|
||||
d := Decision{Suggestion: sug}
|
||||
if created {
|
||||
if replenish != nil {
|
||||
d.ReplenishJobID = &replenish.JobID
|
||||
}
|
||||
return d, nil
|
||||
|
||||
Reference in New Issue
Block a user