swap the index and the theme as one snapshot
Two stores meant a request landing between them saw a new theme with the previous index: each half coherent, the pair a state that never existed on disk. Microseconds, which is why it waited — it closes now because the fix removes machinery instead of adding it. web.Snapshot holds both behind one atomic.Pointer that a rebuild stores once. A Renderer never changes after New, so Refresh and the atomic inside the renderer are gone; an immutable renderer is the simpler object, and the one place a change is applied is now the one place it is observed. web.Handler lost its renderer argument and twenty test construction sites moved with it. Live reload verified on the real binary through the new path: a template edit appeared and reverted, two rebuilds for two edits. Also answers the session's open question: no subagents for fan-out reads. A verdict arriving without the reading behind it cannot be audited, which is the thing this harness exists to make possible. Latent list: 6.
This commit is contained in:
@@ -38,7 +38,7 @@ func assetHandler(t *testing.T) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
|
||||
return Handler(Fixed(content.NewSite(bundles), r), fsys, nil, content.Settings{})
|
||||
}
|
||||
|
||||
func TestABundlesOwnFilesAreServed(t *testing.T) {
|
||||
|
||||
@@ -53,7 +53,7 @@ func benchHandler(b *testing.B, pictures int) http.Handler {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
|
||||
return Handler(Fixed(content.NewSite(bundles), r), fsys, nil, content.Settings{})
|
||||
}
|
||||
|
||||
func serveOnce(b *testing.B, h http.Handler, path string) {
|
||||
|
||||
@@ -29,7 +29,7 @@ func crawlerHandler(t *testing.T, settings content.Settings, extra fstest.MapFS)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, settings)
|
||||
return Handler(Fixed(content.NewSite(bundles), r), fsys, nil, settings)
|
||||
}
|
||||
|
||||
func TestSitemapListsEveryVariantAbsolutely(t *testing.T) {
|
||||
|
||||
@@ -32,7 +32,7 @@ func extrasHandler(t *testing.T, fsys fstest.MapFS) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
|
||||
return Handler(Fixed(content.NewSite(bundles), r), fsys, nil, content.Settings{})
|
||||
}
|
||||
|
||||
func TestExtrasAreNotBundles(t *testing.T) {
|
||||
|
||||
@@ -30,7 +30,7 @@ func feedHandler(t *testing.T, settings content.Settings) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, settings)
|
||||
return Handler(Fixed(content.NewSite(bundles), r), fsys, nil, settings)
|
||||
}
|
||||
|
||||
func fetchFeed(t *testing.T, h http.Handler, path string) (*httptest.ResponseRecorder, atom) {
|
||||
|
||||
@@ -21,7 +21,7 @@ func slugHandler(t *testing.T, fsys fstest.MapFS) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
|
||||
return Handler(Fixed(content.NewSite(bundles), r), fsys, nil, content.Settings{})
|
||||
}
|
||||
|
||||
func TestASlugRenamesTheAddressInEveryLanguage(t *testing.T) {
|
||||
@@ -82,7 +82,7 @@ func TestListingsAndSitemapsUseTheSluggedAddress(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, settings)
|
||||
h := Handler(Fixed(content.NewSite(bundles), r), fsys, nil, settings)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/", nil))
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestNothingInsideAnUnpublishedBundleIsServed(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hidden := Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
|
||||
hidden := Handler(Fixed(content.NewSite(bundles), r), fsys, nil, content.Settings{})
|
||||
for path, want := range map[string]int{
|
||||
"/art/draft/": http.StatusNotFound,
|
||||
"/art/draft/one.jpg": http.StatusNotFound,
|
||||
@@ -58,7 +58,7 @@ func TestNothingInsideAnUnpublishedBundleIsServed(t *testing.T) {
|
||||
// Revealing them is the only thing that changes the answer.
|
||||
site := content.NewSite(bundles)
|
||||
site.Reveal()
|
||||
shown := Handler(Fixed(site), r, fsys, nil, content.Settings{})
|
||||
shown := Handler(Fixed(site, r), fsys, nil, content.Settings{})
|
||||
for _, path := range []string{"/art/draft/", "/art/draft/one.jpg", "/art/future/", "/art/future/two.jpg"} {
|
||||
rec := httptest.NewRecorder()
|
||||
shown.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
@@ -82,7 +82,7 @@ func TestUnpublishedBundlesAreAbsentFromEverythingThatLists(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, settings)
|
||||
h := Handler(Fixed(content.NewSite(bundles), r), fsys, nil, settings)
|
||||
for _, path := range []string{"/art/", "/feed.xml", "/sitemap.xml"} {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
|
||||
+21
-8
@@ -12,22 +12,35 @@ import (
|
||||
"khosra/internal/render"
|
||||
)
|
||||
|
||||
// Current returns the site as it is right now.
|
||||
// Snapshot is the content and the theme that were current together.
|
||||
//
|
||||
// A function rather than a pointer, so a background poller can swap what it returns and a request still sees one
|
||||
// coherent index instead of one being rebuilt underneath it (ADR-0022).
|
||||
type Current func() *content.Site
|
||||
// One value rather than two, because a page assembled from a new theme and the previous index is a page that
|
||||
// never existed on disk. A rebuild makes both and swaps them in one store (ADR-0077).
|
||||
type Snapshot struct {
|
||||
Site *content.Site
|
||||
Theme *render.Renderer
|
||||
}
|
||||
|
||||
// Current returns the snapshot as it is right now.
|
||||
//
|
||||
// A function rather than a pointer, so a background poller can swap what it returns and a request still sees
|
||||
// one coherent pair instead of one being rebuilt underneath it (ADR-0022).
|
||||
type Current func() *Snapshot
|
||||
|
||||
// Fixed is a Current for a site that never changes, which is every caller that does not watch for changes.
|
||||
func Fixed(site *content.Site) Current { return func() *content.Site { return site } }
|
||||
func Fixed(site *content.Site, theme *render.Renderer) Current {
|
||||
snap := &Snapshot{Site: site, Theme: theme}
|
||||
return func() *Snapshot { return snap }
|
||||
}
|
||||
|
||||
// Handler serves a site.
|
||||
//
|
||||
// One mux entry, because URL shape is the resolver's business rather than the mux's: see resolve.
|
||||
func Handler(current Current, r *render.Renderer, siteFS, derivedFS fs.FS, settings content.Settings) http.Handler {
|
||||
func Handler(current Current, siteFS, derivedFS fs.FS, settings content.Settings) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /", func(w http.ResponseWriter, req *http.Request) {
|
||||
serve(w, req, current(), r, siteFS, settings)
|
||||
now := current()
|
||||
serve(w, req, now.Site, now.Theme, siteFS, settings)
|
||||
})
|
||||
// Two exact paths a crawler asks for by name, so they are mux entries rather than resolver cases: no
|
||||
// bundle can own them, since a key always sits under a section.
|
||||
@@ -35,7 +48,7 @@ func Handler(current Current, r *render.Renderer, siteFS, derivedFS fs.FS, setti
|
||||
serveRobots(w, req, siteFS, settings.Base)
|
||||
})
|
||||
mux.HandleFunc("GET "+sitemapPath, func(w http.ResponseWriter, req *http.Request) {
|
||||
serveSitemap(w, req, current(), settings.Base)
|
||||
serveSitemap(w, req, current().Site, settings.Base)
|
||||
})
|
||||
if siteFS != nil {
|
||||
if sub, err := fs.Sub(siteFS, "static"); err == nil {
|
||||
|
||||
@@ -28,7 +28,7 @@ func testHandler(t *testing.T) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
|
||||
return Handler(Fixed(content.NewSite(bundles), r), fsys, nil, content.Settings{})
|
||||
}
|
||||
|
||||
func TestServeBundleAtItsPermalink(t *testing.T) {
|
||||
@@ -72,7 +72,7 @@ func TestTheRootListsEverything(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bare := Handler(Fixed(content.NewSite(nil)), empty, nil, nil, content.Settings{})
|
||||
bare := Handler(Fixed(content.NewSite(nil), empty), nil, nil, content.Settings{})
|
||||
rec = httptest.NewRecorder()
|
||||
bare.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
if rec.Code != http.StatusNotFound {
|
||||
@@ -106,7 +106,7 @@ func multilingualHandler(t *testing.T) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
|
||||
return Handler(Fixed(content.NewSite(bundles), r), fsys, nil, content.Settings{})
|
||||
}
|
||||
|
||||
func TestPrefixedLanguageServesThatVariant(t *testing.T) {
|
||||
@@ -158,7 +158,7 @@ func aliasHandler(t *testing.T) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
|
||||
return Handler(Fixed(content.NewSite(bundles), r), fsys, nil, content.Settings{})
|
||||
}
|
||||
|
||||
func TestAliasRedirectsToCanonical(t *testing.T) {
|
||||
@@ -205,7 +205,7 @@ func listingHandler(t *testing.T, n int) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
|
||||
return Handler(Fixed(content.NewSite(bundles), r), fsys, nil, content.Settings{})
|
||||
}
|
||||
|
||||
func TestSectionIndexListsNewestFirst(t *testing.T) {
|
||||
@@ -273,7 +273,7 @@ func TestStaticFilesAreServedAndDirectoriesAreNot(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
|
||||
h := Handler(Fixed(content.NewSite(bundles), r), fsys, nil, content.Settings{})
|
||||
for path, want := range map[string]int{
|
||||
"/static/style.css": http.StatusOK,
|
||||
"/static/img/logo.svg": http.StatusOK,
|
||||
@@ -314,7 +314,7 @@ func TestAStaticPathThatEscapesTheRootIs404(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := Handler(Fixed(content.NewSite(nil)), r, fsys, nil, content.Settings{})
|
||||
h := Handler(Fixed(content.NewSite(nil), r), fsys, nil, content.Settings{})
|
||||
for path, want := range map[string]int{
|
||||
"/static/ok.css": http.StatusOK,
|
||||
"/static/escape.txt": http.StatusNotFound,
|
||||
@@ -347,7 +347,7 @@ func seriesHandler(t *testing.T) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, nil, nil, content.Settings{})
|
||||
return Handler(Fixed(content.NewSite(bundles), r), nil, nil, content.Settings{})
|
||||
}
|
||||
|
||||
func TestSequenceNavigationLinksNeighbours(t *testing.T) {
|
||||
@@ -439,7 +439,7 @@ func tagHandler(t *testing.T) http.Handler {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Handler(Fixed(content.NewSite(bundles)), r, nil, nil, content.Settings{})
|
||||
return Handler(Fixed(content.NewSite(bundles), r), nil, nil, content.Settings{})
|
||||
}
|
||||
|
||||
func TestGlobalTagListingSpansSectionsGroupedByOne(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user