fix(19.3-06): queue site-cache purges after commit and coalesce them (WR-01)
- PurgeSiteCacheAfterCommit queues the purge on a per-purger worker after commit: one request in flight, saves committed meanwhile collapse into one follow-up, so the admin request never waits for the site - new cooler.SiteCachePurger.Wait drains the queue; Handle.Close waits for it at shutdown (timeout plus one second) - Purge stays the synchronous send; exported names and signatures unchanged
This commit is contained in:
@@ -244,6 +244,16 @@ func newTestPurger(t *testing.T, srv *httptest.Server) *SiteCachePurger {
|
||||
return p
|
||||
}
|
||||
|
||||
// waitPurges drains the purges queued on p, failing the test after 5 s.
|
||||
func waitPurges(t *testing.T, p *SiteCachePurger) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := p.Wait(ctx); err != nil {
|
||||
t.Fatalf("Wait: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertNoToken(t *testing.T, logs *syncBuffer) {
|
||||
t.Helper()
|
||||
for _, line := range logs.lines() {
|
||||
@@ -371,10 +381,12 @@ func TestSiteCachePurgeRequest(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("outside a transaction the after-commit purge runs at once", func(t *testing.T) {
|
||||
t.Run("outside a transaction the after-commit purge is queued at once", func(t *testing.T) {
|
||||
captureLogs(t)
|
||||
srv, hits := purgeReceiver(t, func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{"purged":2}`)) })
|
||||
PurgeSiteCacheAfterCommit(context.Background(), nil, newTestPurger(t, srv), "category", "now")
|
||||
p := newTestPurger(t, srv)
|
||||
PurgeSiteCacheAfterCommit(context.Background(), nil, p, "category", "now")
|
||||
waitPurges(t, p)
|
||||
if got := hits(); len(got) != 1 || got[0].body["kind"] != "category" || got[0].body["slug"] != "now" {
|
||||
t.Fatalf("requests = %#v, want one", got)
|
||||
}
|
||||
@@ -464,6 +476,7 @@ func TestSiteCacheAfterCommitOnly(t *testing.T) {
|
||||
if n := countPosts(t, gdb, "rolled-back"); n != 0 {
|
||||
t.Fatalf("%d rows after rollback", n)
|
||||
}
|
||||
waitPurges(t, p)
|
||||
if got := hits(); len(got) != 0 {
|
||||
t.Fatalf("a rolled-back save sent %d purges", len(got))
|
||||
}
|
||||
@@ -485,6 +498,7 @@ func TestSiteCacheAfterCommitOnly(t *testing.T) {
|
||||
if during != 0 {
|
||||
t.Fatalf("%d purges were sent before the commit", during)
|
||||
}
|
||||
waitPurges(t, p)
|
||||
got := hits()
|
||||
if len(got) != 1 || got[0].body["kind"] != "post" || got[0].body["slug"] != "committed" {
|
||||
t.Fatalf("purges = %#v, want one for post committed", got)
|
||||
@@ -510,6 +524,7 @@ func TestSiteCacheAfterCommitOnly(t *testing.T) {
|
||||
if afterInner != before {
|
||||
t.Fatal("the purge ran when the inner transaction finished")
|
||||
}
|
||||
waitPurges(t, p)
|
||||
if got := hits(); len(got) != before+1 || got[len(got)-1].body["slug"] != "nested" {
|
||||
t.Fatalf("purges = %#v, want one more for nested", got)
|
||||
}
|
||||
@@ -531,6 +546,7 @@ func TestSiteCacheFailureNeverFails(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("a failing purge failed the save: %v", err)
|
||||
}
|
||||
waitPurges(t, p)
|
||||
if n := countPosts(t, gdb, "server-error"); n != 1 || len(hits()) != 1 {
|
||||
t.Fatalf("rows %d, purges %d; want 1 and 1", n, len(hits()))
|
||||
}
|
||||
@@ -559,12 +575,13 @@ func TestSiteCacheFailureNeverFails(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("a slow purge failed the save: %v", err)
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed > SiteCachePurgeTimeout+2*time.Second {
|
||||
t.Fatalf("the save waited %s, want the purge bounded by %s", elapsed, SiteCachePurgeTimeout)
|
||||
if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
|
||||
t.Fatalf("the save waited %s for a queued purge", elapsed)
|
||||
}
|
||||
if n := countPosts(t, gdb, "slow"); n != 1 {
|
||||
t.Fatalf("%d rows, want the save committed", n)
|
||||
}
|
||||
waitPurges(t, p)
|
||||
if lines := logs.lines(); len(lines) != 1 || !strings.Contains(lines[0], "error=timeout") {
|
||||
t.Fatalf("log = %q", lines)
|
||||
}
|
||||
@@ -582,6 +599,7 @@ func TestSiteCacheFailureNeverFails(t *testing.T) {
|
||||
if err != nil || countPosts(t, gdb, "unreachable") != 1 {
|
||||
t.Fatalf("Transaction = %v, rows %d", err, countPosts(t, gdb, "unreachable"))
|
||||
}
|
||||
waitPurges(t, p)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -660,3 +678,42 @@ func TestSiteCachePurgeNoRedirect(t *testing.T) {
|
||||
}
|
||||
assertNoToken(t, logs)
|
||||
}
|
||||
|
||||
func TestSiteCachePurgeAsyncCoalesced(t *testing.T) {
|
||||
logs := captureLogs(t)
|
||||
release := make(chan struct{})
|
||||
srv, hits := purgeReceiver(t, func(w http.ResponseWriter, _ *http.Request) {
|
||||
<-release
|
||||
_, _ = w.Write([]byte(`{"purged":3}`))
|
||||
})
|
||||
p := newTestPurger(t, srv)
|
||||
released := false
|
||||
defer func() {
|
||||
if !released {
|
||||
close(release)
|
||||
}
|
||||
waitPurges(t, p)
|
||||
}()
|
||||
for i := range 20 {
|
||||
start := time.Now()
|
||||
PurgeSiteCacheAfterCommit(context.Background(), nil, p, "post", "slug-"+strconv.Itoa(i))
|
||||
if elapsed := time.Since(start); elapsed > 50*time.Millisecond {
|
||||
t.Fatalf("call %d blocked for %s", i, elapsed)
|
||||
}
|
||||
}
|
||||
close(release)
|
||||
released = true
|
||||
waitPurges(t, p)
|
||||
got := hits()
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("%d requests, want 2 (one in flight, one coalesced follow-up)", len(got))
|
||||
}
|
||||
if got[0].body["slug"] != "slug-0" || got[1].body["slug"] != "slug-19" {
|
||||
t.Fatalf("slugs %q, %q; want slug-0 then the last, slug-19", got[0].body["slug"], got[1].body["slug"])
|
||||
}
|
||||
lines := logs.lines()
|
||||
if len(lines) != 2 || strings.Contains(lines[0], "coalesced=") || !strings.Contains(lines[1], "slug=slug-19") || !strings.Contains(lines[1], "coalesced=18") {
|
||||
t.Fatalf("log = %q, want the second purged line with coalesced=18", lines)
|
||||
}
|
||||
assertNoToken(t, logs)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user