Skip to content
quarry Artifact [dcc7f12010]
Login

Artifact [dcc7f12010]

Artifact dcc7f12010fdbb1564fdd8fb20bd540ef66b8aa38ac5173af9d6c610c15eb6db:


package oltp

import (
	"log"
	"time"
)

// Auto-accept (RFC 0009 §verdicts, the follow-through): a landing a
// decider has armed lands itself when its required checks go green.
// Forge-local OLTP like the check rules beside it — never part of the
// DAG or sync.

const autoSchema = `
CREATE TABLE IF NOT EXISTS landing_auto (
	repo    TEXT NOT NULL,
	landing TEXT NOT NULL,
	account TEXT NOT NULL, -- whose hand writes the deferred accept
	PRIMARY KEY (repo, landing)
);`

// autoMigrations are the additive changes an existing file needs.
var autoMigrations = []string{
	// What the last sweep ran into, so the PAGE can say what an armed
	// landing is waiting on.
	//
	// Recorded rather than recomputed. The page could ask the same
	// predicates the deferred accept asks, and would then be a second
	// implementation of "would this land", free to drift from the one
	// that matters. This is the sweep's own answer, which cannot.
	//
	// It also makes a stuck arm visible: the sweep logs nothing for the
	// ordinary red-checks wait, correctly, so before this an arm that
	// was waiting on something else looked exactly like one that was
	// working.
	`ALTER TABLE landing_auto ADD COLUMN waiting TEXT NOT NULL DEFAULT ''`,
	`ALTER TABLE landing_auto ADD COLUMN waiting_at TEXT NOT NULL DEFAULT ''`,
}

// AutoAccept is one armed landing.
type AutoAccept struct{ Repo, Landing, Account string }

// AutoState is an arm as a page reads it: whose hand, and what the last
// sweep ran into. Waiting is empty when no sweep has reported anything
// — a fresh arm, or one whose last try only found red checks.
type AutoState struct {
	Account string
	Waiting string
	At      string
}

// SetAutoAccept arms a landing; re-arming moves it to the new hand.
func (d *DB) SetAutoAccept(repo, landing, account string) error {
	_, err := d.w.Exec(`INSERT INTO landing_auto (repo, landing, account) VALUES (?,?,?)
		ON CONFLICT(repo, landing) DO UPDATE SET account=excluded.account`, repo, landing, account)
	return err
}

// ClearAutoAccept disarms; disarming what is not armed is harmless.
func (d *DB) ClearAutoAccept(repo, landing string) error {
	_, err := d.w.Exec(`DELETE FROM landing_auto WHERE repo=? AND landing=?`, repo, landing)
	return err
}

// AutoAcceptOf reports who armed a landing, if anyone.
func (d *DB) AutoAcceptOf(repo, landing string) (string, bool) {
	st, ok := d.AutoStateOf(repo, landing)
	return st.Account, ok
}

// AutoStateOf is AutoAcceptOf plus what the last sweep ran into.
func (d *DB) AutoStateOf(repo, landing string) (AutoState, bool) {
	var st AutoState
	err := d.db.QueryRow(`SELECT account, waiting, waiting_at
		FROM landing_auto WHERE repo=? AND landing=?`,
		repo, landing).Scan(&st.Account, &st.Waiting, &st.At)
	return st, err == nil
}

// SetAutoWaiting records what the last sweep ran into, or clears it
// when the try got further than last time. Never allowed to fail the
// sweep: this is a note for a reader, not part of landing.
func (d *DB) SetAutoWaiting(repo, landing, reason string) {
	at := ""
	if reason != "" {
		at = time.Now().UTC().Format(time.RFC3339)
	}
	_, _ = d.w.Exec(`UPDATE landing_auto SET waiting=?, waiting_at=?
		WHERE repo=? AND landing=?`, reason, at, repo, landing)
}

// AutoAccepts lists everything armed, for the sweep. An arm whose
// account no longer exists is disarmed first: the sweep accepts as the
// arming account, and a deleted one must not keep acting.
func (d *DB) AutoAccepts() ([]AutoAccept, error) {
	res, err := d.w.Exec(`DELETE FROM landing_auto WHERE account NOT IN (SELECT name FROM account)`)
	if err != nil {
		return nil, err
	}
	if n, _ := res.RowsAffected(); n > 0 {
		log.Printf("auto-accept: disarmed %d arm(s) held by an account that no longer exists", n)
	}
	rows, err := d.db.Query(`SELECT repo, landing, account FROM landing_auto ORDER BY repo, landing`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []AutoAccept
	for rows.Next() {
		var a AutoAccept
		if err := rows.Scan(&a.Repo, &a.Landing, &a.Account); err != nil {
			return nil, err
		}
		out = append(out, a)
	}
	return out, rows.Err()
}