Author SHA1 Message Date
sneak d9497319c3 Bring TODO.md Status and Next Step to the run in progress (closes #251)
check / check (push) Successful in 6m29s
Status and Next Step described the 24-hour run of 9bd0e91, whose outcome
was posted on issue 226 on 3 November. They now describe the run of
e58c663 on issue 232: at least 24 hours, then on until the feed stalls or
reconnects, up to 72 hours, and what its outcome reports. Two Completed
Steps entries of 10 November are rewritten: the ProtonVPN entry ran past
30 words, and the busy-host test entry referred to waits it had not named.
The PTR entry defines TTL. An entry for this read is added.

Model: fable-5-1
2026-11-10 16:32:28 +00:00
sneak 7e8aae1875 Rewrite the README sentences that fail the prose rules or disagree with the code
A fresh read of README.md whole against the rules of issue 157, with every
statement except measured figures and what outside services do checked
against the code on next. Four sentences fail the prose rules, all in
sections added since 742e759: the origin change definition, the first uses
of TTL and SOA record, and the geojs request. Five disagree with the code:
the geojs request carries only the addresses the cache does not hold; after
a write fails on SQLite's hard heap limit, the prefix handler also writes
its origin changes again, and the PTR and geolocation caches write their
answers again; the test rerun on failure drops -cover; and latitude and
longitude are in degrees, which their names do not say.

Model: fable-5-1
2026-11-10 16:32:04 +00:00
5 changed files with 47 additions and 79 deletions

No files matched your search

-7
View File
@@ -57,13 +57,6 @@ File an issue for each problem the run finds.
# Completed Steps # Completed Steps
- 2026-11-11: two more tests no longer fail on a busy host. The test of the home
page's card refresh runs on a fake clock. The geo test in which geojs never
answers counts each request as it is sent, not when it reaches the server
(closes https://git.eeqj.de/sneak/routewatch/issues/256).
- 2026-11-10: `github.com/mattn/go-sqlite3` is at v1.14.53, so building on the
host no longer prints its two C compiler warnings (closes
https://git.eeqj.de/sneak/routewatch/issues/245).
- 2026-11-10: `README.md` and this file are read whole against the rules of - 2026-11-10: `README.md` and this file are read whole against the rules of
https://git.eeqj.de/sneak/routewatch/issues/157, and every statement except https://git.eeqj.de/sneak/routewatch/issues/157, and every statement except
measured figures and what outside services do is checked against the code on measured figures and what outside services do is checked against the code on
+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/google/btree v1.1.3 github.com/google/btree v1.1.3
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/mattn/go-sqlite3 v1.14.53 github.com/mattn/go-sqlite3 v1.14.29
github.com/maxmind/mmdbwriter v1.2.0 github.com/maxmind/mmdbwriter v1.2.0
github.com/miekg/dns v1.1.73 github.com/miekg/dns v1.1.73
github.com/oschwald/maxminddb-golang/v2 v2.9.0 github.com/oschwald/maxminddb-golang/v2 v2.9.0
+2 -2
View File
@@ -10,8 +10,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/mattn/go-sqlite3 v1.14.53 h1:mnTM0owzUMrKuLo+0w51NUiB0ir2Fo7kFXlPt/l3BGs= github.com/mattn/go-sqlite3 v1.14.29 h1:1O6nRLJKvsi1H2Sj0Hzdfojwt8GiGKm+LOfLaBFaouQ=
github.com/mattn/go-sqlite3 v1.14.53/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/mattn/go-sqlite3 v1.14.29/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/maxmind/mmdbwriter v1.2.0 h1:hyvDopImmgvle3aR8AaddxXnT0iQH2KWJX3vNfkwzYM= github.com/maxmind/mmdbwriter v1.2.0 h1:hyvDopImmgvle3aR8AaddxXnT0iQH2KWJX3vNfkwzYM=
github.com/maxmind/mmdbwriter v1.2.0/go.mod h1:EQmKHhk2y9DRVvyNxwCLKC5FrkXZLx4snc5OlLY5XLE= github.com/maxmind/mmdbwriter v1.2.0/go.mod h1:EQmKHhk2y9DRVvyNxwCLKC5FrkXZLx4snc5OlLY5XLE=
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
+16 -24
View File
@@ -90,12 +90,14 @@ func parseAddresses(t *testing.T, addresses ...string) []netip.Addr {
return addrs return addrs
} }
// fakeGeoJS is a geojs server. It sends body, or status when that is set. It // fakeGeoJS is a geojs server. It sends body, or status when that is set, or
// keeps each request's address list. // waits for the request to end when hang is true. It keeps each request's
// address list.
type fakeGeoJS struct { type fakeGeoJS struct {
server *httptest.Server server *httptest.Server
body string body string
status int status int
hang bool
mu sync.Mutex mu sync.Mutex
requests []string requests []string
@@ -117,6 +119,12 @@ func (f *fakeGeoJS) serve(w http.ResponseWriter, r *http.Request) {
f.requests = append(f.requests, r.URL.Query().Get("ip")) f.requests = append(f.requests, r.URL.Query().Get("ip"))
f.mu.Unlock() f.mu.Unlock()
if f.hang {
<-r.Context().Done()
return
}
if f.status != 0 { if f.status != 0 {
w.WriteHeader(f.status) w.WriteHeader(f.status)
@@ -134,20 +142,6 @@ func (f *fakeGeoJS) requestsSeen() []string {
return append([]string{}, f.requests...) return append([]string{}, f.requests...)
} }
// hangingTransport never answers: each request waits until its context ends.
// It counts the requests as they are sent, so the count does not depend on
// whether one reaches a server before its time limit runs out.
type hangingTransport struct {
requests atomic.Int32
}
func (h *hangingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
h.requests.Add(1)
<-r.Context().Done()
return nil, r.Context().Err()
}
// openCacheDB opens a new cache.db in a temporary directory. // openCacheDB opens a new cache.db in a temporary directory.
func openCacheDB(t *testing.T, dir string) *sql.DB { func openCacheDB(t *testing.T, dir string) *sql.DB {
t.Helper() t.Helper()
@@ -391,15 +385,13 @@ func TestLookupFallsBackToTheDatabase(t *testing.T) {
fake := newFakeGeoJS(t, tt.body) fake := newFakeGeoJS(t, tt.body)
fake.status = tt.status fake.status = tt.status
fake.hang = tt.hang
dir := t.TempDir() dir := t.TempDir()
locator := newTestLocator(t, fake, openCacheDB(t, dir), dir) locator := newTestLocator(t, fake, openCacheDB(t, dir), dir)
requestCount := func() int { return len(fake.requestsSeen()) } locator.timeLimit = shortTimeLimit
if tt.hang { if !tt.hang {
hanging := &hangingTransport{} locator.timeLimit = testTimeLimit
locator.client = &http.Client{Transport: hanging}
locator.timeLimit = shortTimeLimit
requestCount = func() int { return int(hanging.requests.Load()) }
} }
giveDatabase(t, locator) giveDatabase(t, locator)
@@ -421,8 +413,8 @@ func TestLookupFallsBackToTheDatabase(t *testing.T) {
// asks geojs again. // asks geojs again.
lookupOne(t, locator, testAddress) lookupOne(t, locator, testAddress)
if requests := requestCount(); requests != 2 { if requests := fake.requestsSeen(); len(requests) != 2 {
t.Errorf("%d requests, want 2", requests) t.Errorf("%d requests, want 2", len(requests))
} }
}) })
} }
+28 -45
View File
@@ -275,66 +275,49 @@ func TestPagesServeTheCopyOfTheCards(t *testing.T) {
} }
} }
// TestKeepHomeCardsFreshWorksThemOutEveryInterval runs keepHomeCardsFresh. It // TestKeepHomeCardsFreshWorksThemOutEveryInterval runs keepHomeCardsFresh with
// works the cards out at once, again an interval after the table changes, and // a short interval. It works the cards out at once, again after the table
// stops when its context ends. It runs in a synctest bubble, whose clock is // changes, and stops when its context ends.
// fake, so a stall on the host cannot fail it.
func TestKeepHomeCardsFreshWorksThemOutEveryInterval(t *testing.T) { func TestKeepHomeCardsFreshWorksThemOutEveryInterval(t *testing.T) {
t.Parallel() t.Parallel()
const interval = time.Minute const (
interval = 10 * time.Millisecond
maxWait = 15 * time.Second
)
table := emptyTable() table := emptyTable()
announce(table, "1.0.0.0/24", twoPrefixASN, testPeer(1), testRouteTime(0)) announce(table, "1.0.0.0/24", twoPrefixASN, testPeer(1), testRouteTime(0))
s := newServerWithTable(t, table) s := newServerWithTable(t, table)
// newestRouteOrigin returns the origin AS of the newest route on the ctx, cancel := context.WithCancel(t.Context())
// cards, or 0 when there is none. stopped := make(chan struct{})
newestRouteOrigin := func() int {
cards := s.cards.Load()
if cards == nil || cards.NewestRoute == nil {
return 0
}
return cards.NewestRoute.OriginASN go func() {
s.keepHomeCardsFresh(ctx, interval)
close(stopped)
}()
newestRouteASNIs := func(asn int) bool {
cards := s.cards.Load()
return cards != nil && cards.NewestRoute != nil &&
cards.NewestRoute.OriginASN == asn
} }
synctest.Test(t, func(t *testing.T) { waitUntil(t, maxWait, func() bool { return newestRouteASNIs(twoPrefixASN) })
ctx, cancel := context.WithCancel(t.Context())
stopped := make(chan struct{})
go func() { announce(table, "8.8.8.0/24", newestRouteASN, testPeer(2), testRouteTime(1))
s.keepHomeCardsFresh(ctx, interval) waitUntil(t, maxWait, func() bool { return newestRouteASNIs(newestRouteASN) })
close(stopped)
}()
synctest.Wait() cancel()
if got := newestRouteOrigin(); got != twoPrefixASN { select {
t.Fatalf("at the start, the newest route's AS is %d, want %d", case <-stopped:
got, twoPrefixASN) case <-time.After(maxWait):
} t.Fatal("keepHomeCardsFresh did not stop when its context ended")
}
announce(table, "8.8.8.0/24", newestRouteASN, testPeer(2),
testRouteTime(1))
time.Sleep(interval)
synctest.Wait()
if got := newestRouteOrigin(); got != newestRouteASN {
t.Fatalf("an interval after the table changed, the newest "+
"route's AS is %d, want %d", got, newestRouteASN)
}
cancel()
synctest.Wait()
select {
case <-stopped:
default:
t.Fatal("keepHomeCardsFresh did not stop when its context ended")
}
})
} }
// TestStartedServerWorksOutTheCards starts a server and waits until its home // TestStartedServerWorksOutTheCards starts a server and waits until its home