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:
@@ -428,6 +428,47 @@ func TestAddressChangeGuards(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentRedirectWrites checks that redirect writes are serialised
|
||||
// (WR-10): while one transaction holds A -> B uncommitted, a concurrent
|
||||
// B -> A waits, then sees A -> B and is refused as a loop.
|
||||
func TestConcurrentRedirectWrites(t *testing.T) {
|
||||
env := bootEnv(t)
|
||||
svc := env.seoPlugin(t).Service()
|
||||
newRule := func(source, target string) *models.Redirect {
|
||||
r := models.NewRedirect()
|
||||
r.Source, r.Target = source, target
|
||||
return &r
|
||||
}
|
||||
first := env.db.Begin()
|
||||
if _, err := svc.SaveRedirect(t.Context(), first, newRule("/cw-a", "/cw-b")); err != nil {
|
||||
first.Rollback()
|
||||
t.Fatal(err)
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- env.db.Transaction(func(tx *gorm.DB) error {
|
||||
_, err := svc.SaveRedirect(context.Background(), tx, newRule("/cw-b", "/cw-a"))
|
||||
return err
|
||||
})
|
||||
}()
|
||||
select {
|
||||
case err := <-done:
|
||||
first.Rollback()
|
||||
t.Fatalf("B -> A finished while A -> B was uncommitted: %v", err)
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
}
|
||||
if err := first.Commit().Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var loop *classes.LoopError
|
||||
if err := <-done; !errors.As(err, &loop) {
|
||||
t.Fatalf("B -> A after A -> B committed: err = %v, want a loop", err)
|
||||
}
|
||||
if got := redirectTo(env.redirects(t), "/cw-b"); got != "" {
|
||||
t.Fatalf("/cw-b -> %q was stored", got)
|
||||
}
|
||||
}
|
||||
|
||||
// redirectCount is the number of stored redirects.
|
||||
func (e *testEnv) redirectCount(t *testing.T) int64 {
|
||||
t.Helper()
|
||||
|
||||
Reference in New Issue
Block a user