release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m33s
release / go (push) Successful in 1m49s
release / integration (push) Successful in 5m42s
release / android (push) Successful in 5m59s
release / Build signed APK (releases and dev) (push) Successful in 6m9s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped
Applies rule 32's migration exception: each header still says what the schema was, what changes and what it prevents, but task, milestone, decision and rule ids are replaced by the practice they stood for, the operator quotes by the decision, single-install counts are dropped, and pointers to a deleted package and an external design doc are gone. golang-migrate keeps no checksum, so editing applied migrations' comments is safe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
5.1 KiB
SQL
98 lines
5.1 KiB
SQL
-- Auto re-acquisition of missing files via Lidarr. Before this a missing
|
|
-- file was a dead end: reconcile marks it, every selection path skips it,
|
|
-- the admin surface lists it, and there it sits.
|
|
--
|
|
-- The unit here is the ALBUM, not the track, and that is the design decision
|
|
-- carrying most of the safety. Lidarr acquires releases; there is no
|
|
-- meaningful "fetch me one track" operation, and a track-kind request needs a
|
|
-- recording MBID plenty of files simply don't have. Grouping by album means
|
|
-- a reorganised album with dozens of missing files becomes one request.
|
|
|
|
CREATE TABLE missing_reacquisitions (
|
|
album_id uuid PRIMARY KEY REFERENCES albums (id) ON DELETE CASCADE,
|
|
attempts int NOT NULL DEFAULT 0,
|
|
last_attempt_at timestamptz,
|
|
-- The request this album's most recent attempt produced. SET NULL rather
|
|
-- than CASCADE: a purged request row must not erase the attempt history
|
|
-- that stops us asking again in a loop.
|
|
last_request_id uuid REFERENCES lidarr_requests (id) ON DELETE SET NULL,
|
|
-- Set when the attempt budget is spent. Distinct from "attempts = max"
|
|
-- so the reason survives a later change to the configured maximum, and so
|
|
-- the admin surface can say "gave up on the 3rd of August" rather than
|
|
-- inferring it.
|
|
gave_up_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
CONSTRAINT missing_reacquisitions_attempts_nonneg CHECK (attempts >= 0)
|
|
);
|
|
|
|
-- The sweeper's own read: albums due another attempt, oldest attempt first.
|
|
-- Partial on the not-given-up rows because a spent album is never selected
|
|
-- again and would otherwise grow the index forever.
|
|
CREATE INDEX missing_reacquisitions_due_idx
|
|
ON missing_reacquisitions (last_attempt_at NULLS FIRST)
|
|
WHERE gave_up_at IS NULL;
|
|
|
|
-- Settings, singleton in the style of network_settings (0053). Every value an
|
|
-- admin might want to tune lives here rather than in YAML, changed without a
|
|
-- restart.
|
|
CREATE TABLE reacquisition_settings (
|
|
id boolean PRIMARY KEY DEFAULT true,
|
|
|
|
-- Master switch. Default true: this is meant to happen by itself, and a
|
|
-- feature that ships switched off is a feature nobody finds.
|
|
enabled boolean NOT NULL DEFAULT true,
|
|
|
|
-- How long a file must have been missing before the FIRST attempt. This
|
|
-- is what separates "automatic" from "trigger-happy": a filesystem lies
|
|
-- transiently -- an unmounted volume, a container that started before its
|
|
-- media mount attached, a NAS mid-reboot -- and every one of those
|
|
-- resolves itself well inside a day at no cost. tracks.missing_since is
|
|
-- never re-stamped, so it is a true "gone since" clock to measure
|
|
-- against.
|
|
grace_hours int NOT NULL DEFAULT 24,
|
|
|
|
-- Exponential spacing between attempts: base * 2^(attempts-1), clamped to
|
|
-- backoff_max_hours. 6h -> 12h -> 24h -> 48h by default. An album Lidarr
|
|
-- genuinely cannot find must get quieter, not keep pace.
|
|
backoff_base_hours int NOT NULL DEFAULT 6,
|
|
backoff_max_hours int NOT NULL DEFAULT 168, -- one week
|
|
|
|
-- Attempts before giving up. Three real tries spread over days is enough
|
|
-- to ride out a transient Lidarr/indexer outage; past that the answer is
|
|
-- "this release is not obtainable" and asking again is noise.
|
|
max_attempts int NOT NULL DEFAULT 3,
|
|
|
|
-- Ceiling on requests created per sweep. Album grouping already collapses
|
|
-- the common case, but a genuinely large loss (a whole drive slipping
|
|
-- under reconcile's 25% mark cap) should still trickle rather than dump
|
|
-- hundreds of requests into the queue at once.
|
|
max_per_pass int NOT NULL DEFAULT 20,
|
|
|
|
-- Whether the sweeper approves what it creates. Requests are created
|
|
-- pending and nothing reaches Lidarr until approval, so with this off the
|
|
-- feature is a notification rather than an attempt -- which is why the
|
|
-- default is on. Off is the review-first posture: rows appear in the
|
|
-- admin Requests queue for a human to release.
|
|
auto_approve boolean NOT NULL DEFAULT true,
|
|
|
|
CONSTRAINT reacquisition_settings_singleton CHECK (id = true),
|
|
-- Ranges exist to stop a typo becoming a behaviour change: a 0-hour grace
|
|
-- would fire on every transient unmount, and a 10000-per-pass cap would
|
|
-- defeat the point of having one.
|
|
CONSTRAINT reacquisition_settings_grace_range
|
|
CHECK (grace_hours >= 1 AND grace_hours <= 720),
|
|
CONSTRAINT reacquisition_settings_backoff_base_range
|
|
CHECK (backoff_base_hours >= 1 AND backoff_base_hours <= 168),
|
|
CONSTRAINT reacquisition_settings_backoff_max_range
|
|
CHECK (backoff_max_hours >= 1 AND backoff_max_hours <= 720),
|
|
CONSTRAINT reacquisition_settings_backoff_ordered
|
|
CHECK (backoff_max_hours >= backoff_base_hours),
|
|
CONSTRAINT reacquisition_settings_attempts_range
|
|
CHECK (max_attempts >= 1 AND max_attempts <= 10),
|
|
CONSTRAINT reacquisition_settings_per_pass_range
|
|
CHECK (max_per_pass >= 1 AND max_per_pass <= 200)
|
|
);
|
|
|
|
INSERT INTO reacquisition_settings (id) VALUES (true) ON CONFLICT (id) DO NOTHING;
|