The save-time simulation read the rules at READ COMMITTED without a lock,
so A -> B and B -> A written concurrently both passed. CheckRedirect,
AddressChanged and every batch now take a transaction-level advisory lock
before they read the rules.
The loop and chain path of a redirect notice appended the final target
when it was an absolute URL of the site itself, so a loop read
"/a → /b → /a → https://example.test/a". The site URL now counts as
internal, as it does when the chain is followed.
CheckRedirect loads the stored rules and hands them to
checkRedirectRules, which needs no database, so the loop and chain rules
can be unit-tested. No behaviour change.
- rule set: exact sources by key, patterns by literal prefix, regex by id
(stdlib RE2, compiled once per source); ignore case, trailing slash,
query ignore/pass, a source with ? matches its query exactly
- targets: captures escaped, a relative target never leaves the site, an
absolute one keeps its host
- SaveRedirect validates, simulates the rule in place (exact, pattern and
regex probes, 10 hops) and refuses a loop; chains answer the notice
- resolve follows the chain to its final target, 410 answers gone, a
runtime cycle answers not_found and is logged once