landinglane

package
v0.162.3 Latest Latest
Warning

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

Go to latest
Published: Sep 24, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package landinglane records exclusive ownership of one (repository, target) landing lane so two live sessions never drive `wb pr land` or `wb worktree merge` toward the same target branch at once.

On 2026-09-07 two live sessions landed on sneat-dev/wb main concurrently: main advanced under a published candidate four times in one session, costing a re-prepare or stranding a receipt each time. The working agreement is one landing owner per (repository, target branch); this package is the mechanical enforcement of that agreement.

A lane record is a durable claim, not a running lock: it survives the command that acquired it so a later `wb pr land` / `wb worktree merge` invocation from the same session (resume, retry, a second batch) is recognised as the same owner, while a different live session is refused and named. Mutual exclusion during the read-modify-write itself is a short-lived flock scoped to one Acquire/Release/Heartbeat call, not the whole command.

Index

Constants

View Source
const DefaultStaleAfter = 30 * time.Minute

DefaultStaleAfter is how long a lane owner's heartbeat may go unrefreshed before it is treated as abandoned and taken over automatically. It is deliberately generous: `wb pr land` and `wb worktree merge` can spend real minutes waiting on CI, and a lane must not flap ownership under a slow but live run.

View Source
const DirName = "lanes"

DirName is the directory under WB's home that holds lane ownership records.

View Source
const SchemaVersion = 1

SchemaVersion guards forward-incompatible record shape changes.

Variables

View Source
var ErrTakeoverReasonRequired = errors.New("--take-over-lane requires --lane-reason <text>")

TakeoverReasonRequiredError reports that --take-over-lane was requested without the --lane-reason it must carry into the record and the receipt.

Functions

func Heartbeat

func Heartbeat(home, repository, target, wbSessionID string, now func() time.Time) error

Heartbeat refreshes the current owner's heartbeat timestamp while a landing command keeps running. It is a no-op, not an error, when the caller no longer owns the lane (another session already took it over, or it was already released) so a background ticker never needs to distinguish "lost the lane" from "nothing to do".

func LaneID

func LaneID(repository, target string) string

LaneID deterministically names the (repository, target) landing lane. It mirrors the readable-plus-hash shape used elsewhere in WB (see worktreeMergeLaneID in internal/orchestrate) so a lane id is stable, filesystem-safe, and recognisable at a glance without importing the orchestrate package, which itself depends on this one.

func Release

func Release(home, repository, target, wbSessionID string) error

Release retires this session's ownership of the lane. It is a no-op when the lane is not currently held by wbSessionID, matching the working agreement that a command exiting without an active receipt (or reaching a terminal receipt status) frees the lane rather than leaving a stale claim behind for the stale-timeout to eventually clear.

Types

type AcquireRequest

type AcquireRequest struct {
	Repository string
	Target     string
	Self       Owner

	// SessionDir is the WB session registry directory used to evaluate the
	// current owner's liveness (see internal/session). Required unless
	// IsOwnerLive is supplied directly, e.g. by a test.
	SessionDir string
	// IsOwnerLive overrides the default session-registry liveness check.
	// Tests inject this to avoid depending on real processes; production
	// callers should leave it nil.
	IsOwnerLive func(Owner) bool

	StaleAfter time.Duration
	Now        func() time.Time

	TakeOver       bool
	TakeoverReason string
}

AcquireRequest describes one attempt to acquire or refresh a landing lane.

type ConflictError

type ConflictError struct {
	Record Record
}

ConflictError reports that a different live session holds the lane. Error names the owner, its command and receipt, and the only sanctioned overrides, so a refusal is actionable without a second lookup.

func (*ConflictError) Error

func (e *ConflictError) Error() string

type CorruptRecordError

type CorruptRecordError struct {
	Path string
	Err  error
}

CorruptRecordError reports that a lane record exists but could not be parsed. Acquire fails closed on this: an unreadable record must never be treated as an absent one, because that would grant the lane with no conflict check at all — the one case a corrupt file is least allowed to cause. Only an explicit --take-over-lane --lane-reason override replaces it.

func (*CorruptRecordError) Error

func (e *CorruptRecordError) Error() string

func (*CorruptRecordError) Unwrap

func (e *CorruptRecordError) Unwrap() error

type Owner

type Owner struct {
	WBSessionID string `json:"wb_session_id"`
	PID         int    `json:"pid"`
	Runtime     string `json:"runtime,omitempty"`
	Model       string `json:"model,omitempty"`
	// Command names the verb holding the lane, e.g. "wb pr land" or
	// "wb worktree merge land", so a refusal can say what the owner is doing.
	Command string `json:"command,omitempty"`
	// ReceiptPath is the local receipt this owner is driving, when one
	// exists yet (a `wb pr land` run before its first receipt write has
	// none).
	ReceiptPath string    `json:"receipt_path,omitempty"`
	AcquiredAt  time.Time `json:"acquired_at"`
	HeartbeatAt time.Time `json:"heartbeat_at"`
}

Owner identifies the WB session and command driving a landing lane.

type Record

type Record struct {
	SchemaVersion int    `json:"schema_version"`
	Lane          string `json:"lane"`
	Repository    string `json:"repository"`
	Target        string `json:"target"`
	Owner         Owner  `json:"owner"`

	// PriorOwner and the takeover fields are set only on the acquisition
	// that took the lane from a different owner, so a reader can see why
	// ownership moved without replaying history.
	PriorOwner     *Owner `json:"prior_owner,omitempty"`
	TakenOver      bool   `json:"taken_over,omitempty"`
	TakeoverReason string `json:"takeover_reason,omitempty"`
	// TakeoverNote explains an automatic (stale-owner) takeover; it is
	// distinct from TakeoverReason, which only an explicit --take-over-lane
	// --reason override sets.
	TakeoverNote string `json:"takeover_note,omitempty"`
}

Record is the durable state of one (repository, target) landing lane.

func Acquire

func Acquire(home string, request AcquireRequest) (Record, error)

Acquire admits Self as the owner of the (Repository, Target) lane, refusing a different live owner, taking over a stale or dead one, and refreshing the record when Self already owns it.

func Read

func Read(home, repository, target string) (Record, bool, error)

Read returns the current lane record, if any. It takes a shared read of the lock so a reader never observes a torn write.

Jump to

Keyboard shortcuts

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