notice content changes and rebuild without a restart

Polling lives in internal/ext/watch, per the human's call to keep core under its
ceiling rather than raise it a second time — which is what ADR-0041 said a second
raise would mean. It is a poller, deletable without trace, and core stayed at
2671/2800.

A settled change calls the same `rebuilder` that startup calls, because a reload
path that differs from the startup path is a reload path that drifts. The index is
an atomic.Pointer swapped whole, so a request reads the site that was current when
it arrived instead of one being rebuilt underneath it — the alternative, mutating in
place, is a data race with every in-flight request.

Names, sizes and modification times, not contents: reading every file to detect a
change costs more than the rebuild it triggers. Editor droppings are excluded,
because saving in vim writes a swap file, a backup and the number 4913, and each
would otherwise look like a change. A change must hold still for a moment first,
since one save is often several operations.

Verified against the running binary: a page 404s, the file appears, and five seconds
later it serves — one "site root changed" in the log. Then three droppings written
at once produced no rebuild at all.

Two warnings fired and were fixed rather than silenced: `runServe` gave up the
rebuild closure to `rebuilder`, and the fingerprint walk gave up its body to
`record`, where three exclusions read as a list instead of as nesting.

The Dockerfile ships the binary alone. The site root arrives as a volume and is
never copied in — it is somebody's content repository with its own history
(ADR-0011), so the image is the same for every site.
This commit is contained in:
2026-08-01 02:23:37 +06:00
parent 37a3fa6ce6
commit 67fa408bb0
17 changed files with 381 additions and 38 deletions
+8 -8
View File
@@ -28,7 +28,7 @@ func testHandler(t *testing.T) http.Handler {
if err != nil {
t.Fatal(err)
}
return Handler(content.NewSite(bundles), r, fsys, nil, content.Settings{})
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
}
func TestServeBundleAtItsPermalink(t *testing.T) {
@@ -76,7 +76,7 @@ func multilingualHandler(t *testing.T) http.Handler {
if err != nil {
t.Fatal(err)
}
return Handler(content.NewSite(bundles), r, fsys, nil, content.Settings{})
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
}
func TestPrefixedLanguageServesThatVariant(t *testing.T) {
@@ -128,7 +128,7 @@ func aliasHandler(t *testing.T) http.Handler {
if err != nil {
t.Fatal(err)
}
return Handler(content.NewSite(bundles), r, fsys, nil, content.Settings{})
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
}
func TestAliasRedirectsToCanonical(t *testing.T) {
@@ -175,7 +175,7 @@ func listingHandler(t *testing.T, n int) http.Handler {
if err != nil {
t.Fatal(err)
}
return Handler(content.NewSite(bundles), r, fsys, nil, content.Settings{})
return Handler(Fixed(content.NewSite(bundles)), r, fsys, nil, content.Settings{})
}
func TestSectionIndexListsNewestFirst(t *testing.T) {
@@ -243,7 +243,7 @@ func TestStaticFilesAreServedAndDirectoriesAreNot(t *testing.T) {
if err != nil {
t.Fatal(err)
}
h := Handler(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,
@@ -284,7 +284,7 @@ func TestAStaticPathThatEscapesTheRootIs404(t *testing.T) {
if err != nil {
t.Fatal(err)
}
h := Handler(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,
@@ -317,7 +317,7 @@ func seriesHandler(t *testing.T) http.Handler {
if err != nil {
t.Fatal(err)
}
return Handler(content.NewSite(bundles), r, nil, nil, content.Settings{})
return Handler(Fixed(content.NewSite(bundles)), r, nil, nil, content.Settings{})
}
func TestSequenceNavigationLinksNeighbours(t *testing.T) {
@@ -407,7 +407,7 @@ func tagHandler(t *testing.T) http.Handler {
if err != nil {
t.Fatal(err)
}
return Handler(content.NewSite(bundles), r, nil, nil, content.Settings{})
return Handler(Fixed(content.NewSite(bundles)), r, nil, nil, content.Settings{})
}
func TestGlobalTagListingSpansSectionsGroupedByOne(t *testing.T) {