mrqueue

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 22, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package mrqueue provides merge request queue storage and events.

Package mrqueue provides merge request queue storage. MRs are stored locally in .beads/mq/ and deleted after merge. This avoids sync overhead for transient MR state.

Package mrqueue provides merge request queue storage and priority scoring.

MQ Priority Objective Function

The merge queue uses a priority scoring function to determine processing order. Higher scores mean higher priority (process first).

## Scoring Formula

score = BaseScore
      + ConvoyAgeWeight * hoursOld(convoy)              // Prevent starvation
      + PriorityWeight * (4 - priority)                 // P0 > P4
      - min(RetryPenalty * retryCount, MaxRetryPenalty) // Prevent thrashing
      + MRAgeWeight * hoursOld(MR)                      // FIFO tiebreaker

## Default Weights

BaseScore:       1000.0  (keeps all scores positive)
ConvoyAgeWeight:   10.0  (10 pts/hour = 240 pts/day)
PriorityWeight:   100.0  (P0=+400, P4=+0)
RetryPenalty:      50.0  (each retry loses 50 pts)
MRAgeWeight:        1.0  (1 pt/hour, minor FIFO factor)
MaxRetryPenalty:  300.0  (caps at 6 retries worth)

## Design Principles

1. Deterministic: same inputs always produce same score (uses explicit Now param)

  1. Convoy Starvation Prevention: older convoys escalate in priority. A 48-hour old P4 convoy will beat a fresh P0 standalone issue (+480 vs +400).

3. Priority Respect: within similar convoy ages, P0 issues beat P4 issues.

  1. Thrashing Prevention: MRs that repeatedly fail with conflicts get deprioritized, giving the repo state time to stabilize.

5. FIFO Fairness: within same convoy/priority/retry state, older MRs go first.

## Example Scores

Fresh P0, no convoy:                    1400 (1000 + 400)
Fresh P4, no convoy:                    1000 (1000 + 0)
Fresh P2, 24h convoy:                   1440 (1000 + 200 + 240)
Fresh P4, 48h convoy:                   1480 (1000 + 0 + 480)
P2, 24h convoy, 3 retries:              1290 (1000 + 200 + 240 - 150)
P0, no convoy, 6+ retries (capped):     1100 (1000 + 400 - 300)

## Tuning

All weights are configurable via ScoreConfig. The defaults are designed so:

  • A 48-hour convoy beats any standalone priority (starvation prevention)
  • Priority differences dominate within same convoy
  • Retry penalty is significant but capped (eventual progress guaranteed)

Index

Constants

View Source
const ClaimStaleTimeout = 10 * time.Minute

ClaimStaleTimeout is how long before a claimed MR is considered stale. If a worker claims an MR but doesn't process it within this time, another worker can reclaim it.

Variables

View Source
var (
	ErrNotFound       = fmt.Errorf("merge request not found")
	ErrAlreadyClaimed = fmt.Errorf("merge request already claimed by another worker")
)

Common errors for claiming

Functions

func ScoreMR

func ScoreMR(input ScoreInput, config ScoreConfig) float64

ScoreMR calculates the priority score for a merge request. Higher scores mean higher priority (process first).

The scoring formula:

score = BaseScore
      + ConvoyAgeWeight * hoursOld(convoy)       // Prevent convoy starvation
      + PriorityWeight * (4 - priority)          // P0=+400, P4=+0
      - min(RetryPenalty * retryCount, MaxRetryPenalty)  // Prevent thrashing
      + MRAgeWeight * hoursOld(MR)               // FIFO tiebreaker

Design principles:

  • Deterministic: same inputs always produce same score
  • Convoy starvation prevention: older convoys escalate in priority
  • Priority respect: P0 bugs beat P4 backlog items
  • Thrashing prevention: repeated failures get deprioritized
  • FIFO fairness: within same convoy/priority, older MRs go first

func ScoreMRWithDefaults

func ScoreMRWithDefaults(input ScoreInput) float64

ScoreMRWithDefaults is a convenience wrapper using default config.

Types

type BeadStatusChecker

type BeadStatusChecker func(beadID string) (isOpen bool, err error)

BeadStatusChecker is a function type that checks if a bead is open. Returns true if the bead is open (not closed), false if closed or not found.

type Event

type Event struct {
	Timestamp   time.Time `json:"timestamp"`
	Type        EventType `json:"type"`
	MRID        string    `json:"mr_id"`
	Branch      string    `json:"branch"`
	Target      string    `json:"target"`
	Worker      string    `json:"worker,omitempty"`
	SourceIssue string    `json:"source_issue,omitempty"`
	Rig         string    `json:"rig,omitempty"`
	MergeCommit string    `json:"merge_commit,omitempty"` // For merged events
	Reason      string    `json:"reason,omitempty"`       // For failed/skipped events
}

Event represents a single MQ lifecycle event.

type EventLogger

type EventLogger struct {
	// contains filtered or unexported fields
}

EventLogger handles writing MQ events to the event log.

func NewEventLogger

func NewEventLogger(beadsDir string) *EventLogger

NewEventLogger creates a new EventLogger for the given beads directory.

func NewEventLoggerFromRig

func NewEventLoggerFromRig(rigPath string) *EventLogger

NewEventLoggerFromRig creates an EventLogger for the given rig path.

func (*EventLogger) LogEvent

func (l *EventLogger) LogEvent(event Event) error

LogEvent writes an event to the MQ event log.

func (*EventLogger) LogMergeFailed

func (l *EventLogger) LogMergeFailed(mr *MR, reason string) error

LogMergeFailed logs a merge_failed event.

func (*EventLogger) LogMergeSkipped

func (l *EventLogger) LogMergeSkipped(mr *MR, reason string) error

LogMergeSkipped logs a merge_skipped event.

func (*EventLogger) LogMergeStarted

func (l *EventLogger) LogMergeStarted(mr *MR) error

LogMergeStarted logs a merge_started event.

func (*EventLogger) LogMerged

func (l *EventLogger) LogMerged(mr *MR, mergeCommit string) error

LogMerged logs a merged event.

func (*EventLogger) LogPath

func (l *EventLogger) LogPath() string

LogPath returns the path to the event log file.

type EventType

type EventType string

EventType represents the type of MQ lifecycle event.

const (
	// EventMergeStarted indicates refinery began processing an MR.
	EventMergeStarted EventType = "merge_started"
	// EventMerged indicates an MR was successfully merged.
	EventMerged EventType = "merged"
	// EventMergeFailed indicates a merge failed (conflict, tests, etc.).
	EventMergeFailed EventType = "merge_failed"
	// EventMergeSkipped indicates an MR was skipped (already merged, etc.).
	EventMergeSkipped EventType = "merge_skipped"
)

type MR

type MR struct {
	ID          string    `json:"id"`
	Branch      string    `json:"branch"`       // Source branch (e.g., "polecat/nux")
	Target      string    `json:"target"`       // Target branch (e.g., "main")
	SourceIssue string    `json:"source_issue"` // The work item being merged
	Worker      string    `json:"worker"`       // Who did the work
	Rig         string    `json:"rig"`          // Which rig
	Title       string    `json:"title"`        // MR title
	Priority    int       `json:"priority"`     // Priority (lower = higher priority)
	CreatedAt   time.Time `json:"created_at"`
	AgentBead   string    `json:"agent_bead,omitempty"` // Agent bead ID that created this MR (for traceability)

	// Priority scoring fields
	RetryCount      int        `json:"retry_count,omitempty"`       // Conflict retry count for priority penalty
	ConvoyID        string     `json:"convoy_id,omitempty"`         // Parent convoy ID if part of a convoy
	ConvoyCreatedAt *time.Time `json:"convoy_created_at,omitempty"` // Convoy creation time for starvation prevention

	// Claiming fields for parallel refinery workers
	ClaimedBy string     `json:"claimed_by,omitempty"` // Worker ID that claimed this MR
	ClaimedAt *time.Time `json:"claimed_at,omitempty"` // When the MR was claimed

	// Blocking fields for non-blocking delegation
	BlockedBy string `json:"blocked_by,omitempty"` // Task ID that blocks this MR (e.g., conflict resolution task)
}

MR represents a merge request in the queue.

func (*MR) IsBlocked

func (mr *MR) IsBlocked(checkStatus func(beadID string) (isOpen bool, err error)) (bool, string, error)

IsBlocked checks if an MR is blocked by a task that is still open. If blocked, returns true and the blocking task ID. checkStatus is a function that checks if a bead is still open.

func (*MR) Score

func (mr *MR) Score() float64

Score calculates the priority score for this MR using default config. Higher scores mean higher priority (process first).

func (*MR) ScoreAt

func (mr *MR) ScoreAt(now time.Time) float64

ScoreAt calculates the priority score at a specific time (for deterministic testing).

type Queue

type Queue struct {
	// contains filtered or unexported fields
}

Queue manages the MR storage.

func New

func New(rigPath string) *Queue

New creates a new MR queue for the given rig path.

func NewFromWorkdir

func NewFromWorkdir(workdir string) (*Queue, error)

NewFromWorkdir creates a queue by finding the rig root from a working directory.

func (*Queue) Claim

func (q *Queue) Claim(id, workerID string) error

Claim attempts to claim an MR for processing by a specific worker. Returns nil if successful, ErrAlreadyClaimed if another worker has it, or ErrNotFound if the MR doesn't exist. Uses atomic file operations to prevent race conditions.

func (*Queue) ClearBlockedBy

func (q *Queue) ClearBlockedBy(mrID string) error

ClearBlockedBy removes the blocking task from an MR.

func (*Queue) Count

func (q *Queue) Count() int

Count returns the number of pending MRs.

func (*Queue) Dir

func (q *Queue) Dir() string

Dir returns the queue directory path.

func (*Queue) EnsureDir

func (q *Queue) EnsureDir() error

EnsureDir creates the MQ directory if it doesn't exist.

func (*Queue) Get

func (q *Queue) Get(id string) (*MR, error)

Get retrieves a specific MR by ID.

func (*Queue) List

func (q *Queue) List() ([]*MR, error)

List returns all pending MRs, sorted by priority then creation time. Deprecated: Use ListByScore for priority-aware ordering.

func (*Queue) ListBlocked

func (q *Queue) ListBlocked(checkStatus BeadStatusChecker) ([]*MR, error)

ListBlocked returns MRs that are blocked by open tasks. Useful for reporting/monitoring.

func (*Queue) ListByScore

func (q *Queue) ListByScore() ([]*MR, error)

ListByScore returns all pending MRs sorted by priority score (highest first). Uses the ScoreMR function which considers:

  • Convoy age (prevents starvation)
  • Issue priority (P0-P4)
  • Retry count (prevents thrashing)
  • MR age (FIFO tiebreaker)

func (*Queue) ListClaimedBy

func (q *Queue) ListClaimedBy(workerID string) ([]*MR, error)

ListClaimedBy returns MRs claimed by a specific worker.

func (*Queue) ListReady

func (q *Queue) ListReady(checkStatus BeadStatusChecker) ([]*MR, error)

ListReady returns MRs that are ready for processing: - Not claimed by another worker (or claim is stale) - Not blocked by an open task Sorted by priority score (highest first). The checkStatus function is used to check if blocking tasks are still open.

func (*Queue) ListUnclaimed

func (q *Queue) ListUnclaimed() ([]*MR, error)

ListUnclaimed returns MRs that are not claimed or have stale claims. Sorted by priority then creation time.

func (*Queue) Release

func (q *Queue) Release(id string) error

Release releases a claimed MR back to the queue. Called when processing fails and the MR should be retried.

func (*Queue) Remove

func (q *Queue) Remove(id string) error

Remove deletes an MR from the queue (after successful merge).

func (*Queue) SetBlockedBy

func (q *Queue) SetBlockedBy(mrID, taskID string) error

SetBlockedBy marks an MR as blocked by a task (e.g., conflict resolution). When the blocking task closes, the MR becomes ready for processing again.

func (*Queue) Submit

func (q *Queue) Submit(mr *MR) error

Submit adds a new MR to the queue.

type ScoreConfig

type ScoreConfig struct {
	// BaseScore is the starting score before applying factors.
	// Default: 1000 (keeps all scores positive)
	BaseScore float64

	// ConvoyAgeWeight is points added per hour of convoy age.
	// Older convoys get priority to prevent starvation.
	// Default: 10.0 (10 pts/hour = 240 pts/day)
	ConvoyAgeWeight float64

	// PriorityWeight is multiplied by (4 - priority) so P0 gets most points.
	// P0 adds 4*weight, P1 adds 3*weight, ..., P4 adds 0*weight.
	// Default: 100.0 (P0 gets +400, P4 gets +0)
	PriorityWeight float64

	// RetryPenalty is subtracted per retry attempt to prevent thrashing.
	// MRs that keep failing get deprioritized, giving repo state time to stabilize.
	// Default: 50.0 (each retry loses 50 pts)
	RetryPenalty float64

	// MRAgeWeight is points added per hour since MR submission.
	// Minor factor for FIFO ordering within same priority/convoy.
	// Default: 1.0 (1 pt/hour)
	MRAgeWeight float64

	// MaxRetryPenalty caps the total retry penalty to prevent permanent deprioritization.
	// Default: 300.0 (after 6 retries, penalty is capped)
	MaxRetryPenalty float64
}

ScoreConfig contains tunable weights for MR priority scoring. All weights are designed so higher scores = higher priority (process first).

func DefaultScoreConfig

func DefaultScoreConfig() ScoreConfig

DefaultScoreConfig returns sensible defaults for MR scoring.

type ScoreInput

type ScoreInput struct {
	// Priority is the issue priority (0=P0/critical, 4=P4/backlog).
	Priority int

	// MRCreatedAt is when the MR was submitted to the queue.
	MRCreatedAt time.Time

	// ConvoyCreatedAt is when the convoy was created.
	// Nil if MR is not part of a convoy (standalone work).
	ConvoyCreatedAt *time.Time

	// RetryCount is how many times this MR has been retried after conflicts.
	// 0 = first attempt.
	RetryCount int

	// Now is the current time (for deterministic testing).
	// If zero, time.Now() is used.
	Now time.Time
}

ScoreInput contains the data needed to score an MR. This struct decouples scoring from the MR struct, allowing the caller to provide convoy age from external lookups.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL