fix: serialise redirect writes so concurrent saves cannot store a loop (WR-10)
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.
This commit is contained in:
@@ -165,7 +165,14 @@ func classHas(ranges []rune, c rune) bool {
|
||||
// matches comes back round or does not end within MaxRedirectHops, a
|
||||
// chain when it takes more than one hop or when another rule's target
|
||||
// leads into r. A 410 rule ends every chain and is never a loop.
|
||||
//
|
||||
// db must be the writer's transaction: CheckRedirect takes the redirect
|
||||
// write lock (lockRedirectWrites) before it reads, so two concurrent writes
|
||||
// cannot each pass the simulation without the other's rule.
|
||||
func (s *Service) CheckRedirect(ctx context.Context, db *gorm.DB, r models.Redirect) (RedirectCheck, error) {
|
||||
if err := lockRedirectWrites(db.WithContext(ctx)); err != nil {
|
||||
return RedirectCheck{}, err
|
||||
}
|
||||
if r.Status == models.Status410 {
|
||||
return RedirectCheck{}, nil
|
||||
}
|
||||
@@ -176,6 +183,18 @@ func (s *Service) CheckRedirect(ctx context.Context, db *gorm.DB, r models.Redir
|
||||
return checkRedirectRules(s.SiteURL(), rules, r)
|
||||
}
|
||||
|
||||
// redirectLockKey is the transaction-level advisory lock that serialises
|
||||
// redirect writes ("g15seord").
|
||||
const redirectLockKey int64 = 0x6731357365_6f7264
|
||||
|
||||
// lockRedirectWrites takes the redirect write lock for the rest of tx (it
|
||||
// is released on commit or rollback). Every path that simulates a redirect
|
||||
// write takes it before reading the rules (WR-10): at READ COMMITTED the
|
||||
// next statement after the wait sees what the other writer committed.
|
||||
func lockRedirectWrites(tx *gorm.DB) error {
|
||||
return tx.Exec("SELECT pg_advisory_xact_lock(?)", redirectLockKey).Error
|
||||
}
|
||||
|
||||
// checkRedirectRules is CheckRedirect over the stored rules given, for the
|
||||
// site URL site.
|
||||
func checkRedirectRules(site string, rules []models.Redirect, r models.Redirect) (RedirectCheck, error) {
|
||||
|
||||
Reference in New Issue
Block a user