2 Commits
Author SHA1 Message Date
clawbot 1c6f01bc84 Keep a peer-down until its route delete succeeds (closes #101)
check / check (push) Waiting to run
When the delete of a peer's live routes after its session went down
failed, the down was dropped and the routes stayed. The prefix handler
now keeps such a peer, once, with the updates it sent after the down,
for the next flush, which retries the delete before it writes them; each
failure is logged. At most the last 25,000 of those updates are kept,
outside the batch, so they do not start a flush. The delete runs in
chunks of at most 10,000 routes per table, taken in prefix order
through the peer's key, each chunk in its own transaction, so the
database lock is released between chunks and a failure undoes at most
one chunk.

Model: opus-5-5
2026-10-19 21:31:05 +00:00
clawbot 176f9708b8 Build and test with a supported Go release (closes #107)
check / check (push) Canceled after 0s
The test and build stages move from golang:1.24-bookworm, a release
line that no longer gets security fixes, to golang:1.27.2-bookworm,
pinned by digest. The go directive in go.mod moves from 1.24.4 to
1.27.1, the Go in the pinned lint image, so no stage downloads another
toolchain. One test starts its goroutine with sync.WaitGroup.Go, which
the linter asks for once the module targets Go 1.25 or later.

Model: opus-5-5
2026-10-19 23:19:21 +02:00
4 changed files with 36 additions and 29 deletions
+4 -4
View File
@@ -24,8 +24,8 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# image ships and the alpine one does not. -count=1 because the mounted
# cache also holds test results, which go test would otherwise replay.
# -short skips the live RIS Live feed test, so the suite needs no network.
# golang:1.24-bookworm, 2026-09-21
FROM golang@sha256:1a6d4452c65dea36aac2e2d606b01b4a029ec90cc1ae53890540ce6173ea77ac AS test
# golang:1.27.2-bookworm, 2026-10-19
FROM golang@sha256:5cf287a799e6b94384bad13d16b14904c531f51ba65792237e122ce42b392f61 AS test
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
@@ -39,8 +39,8 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# Build stage. Nothing is wanted from either phase above; the copies
# are what make BuildKit build them first, so nothing after them can
# run unless lint and test passed.
# golang:1.24-bookworm, 2026-09-21
FROM golang@sha256:1a6d4452c65dea36aac2e2d606b01b4a029ec90cc1ae53890540ce6173ea77ac AS builder
# golang:1.27.2-bookworm, 2026-10-19
FROM golang@sha256:5cf287a799e6b94384bad13d16b14904c531f51ba65792237e122ce42b392f61 AS builder
# Install build dependencies (zstd for archive, gcc for CGO/sqlite3)
RUN apt-get update && apt-get install -y --no-install-recommends \
+1 -1
View File
@@ -1,6 +1,6 @@
module git.eeqj.de/sneak/routewatch
go 1.24.4
go 1.27.1
require (
github.com/dustin/go-humanize v1.0.1
+2 -5
View File
@@ -564,11 +564,8 @@ func TestBatchWriteDuringCheckpoint(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
for {
select {
case <-ctx.Done():
@@ -577,7 +574,7 @@ func TestBatchWriteDuringCheckpoint(t *testing.T) {
_ = db.Checkpoint(ctx) // errors are the checkpoint's own to absorb
}
}
}()
})
ts := time.Now().UTC()
for i := range contentionIterations {
@@ -597,11 +597,11 @@ func (s *firstPeerDeleteFailsStore) DeleteLiveRoutesOfPeer(
// after another: a peer's announcement, then its session going down and a new
// announcement from the peer, then nothing. The failed delete must be retried
// on the next flush, and the announcement the peer sent after the down written
// only after that retry, so it is kept. In the second case the peer's session
// only after that retry, so it is kept. In the other cases the peer's session
// goes down again, and the peer announces another prefix, while the delete
// fails: the announcement sent between the two downs must not be written, and
// the next flush must delete the peer's routes once, not once for the retry and
// once for the down.
// fails or after the flush whose delete failed: the announcement sent between
// the two downs must not be written, and the next flush must delete the peer's
// routes once, not once for the retry and once for the down.
func TestPrefixHandlerRetriesAFailedDeleteOfAPeersRoutes(t *testing.T) {
t.Parallel()
@@ -610,37 +610,42 @@ func TestPrefixHandlerRetriesAFailedDeleteOfAPeersRoutes(t *testing.T) {
afterDown = "198.51.100.0/24"
afterSecondDown = "203.0.113.0/24"
maskLength = 24
// The delete that fails and its retry, which is also the delete for
// a second down.
wantDeletes = 2
)
tests := []struct {
name string
// Handled while the delete fails, if not nil.
whileFailing []*ristypes.RISMessage
wantDeletes int
// Handled after the flush whose delete fails, if not nil.
afterFailing []*ristypes.RISMessage
// The peers left with a live route for each prefix.
wantPeers map[string][]string
}{
{
name: "the delete fails once",
wantDeletes: 2,
name: "the delete fails once",
wantPeers: map[string][]string{
beforeDown: nil,
afterDown: {testPeerIP},
afterSecondDown: nil,
beforeDown: nil, afterDown: {testPeerIP}, afterSecondDown: nil,
},
},
{
name: "the session goes down again while the delete fails",
whileFailing: []*ristypes.RISMessage{
sessionDown(),
peerAnnouncement(testPeerIP, afterSecondDown),
sessionDown(), peerAnnouncement(testPeerIP, afterSecondDown),
},
// One retry, which is also the delete for the second down.
wantDeletes: 2,
wantPeers: map[string][]string{
beforeDown: nil,
afterDown: nil,
afterSecondDown: {testPeerIP},
beforeDown: nil, afterDown: nil, afterSecondDown: {testPeerIP},
},
},
{
name: "the session goes down again after the delete failed",
afterFailing: []*ristypes.RISMessage{
sessionDown(), peerAnnouncement(testPeerIP, afterSecondDown),
},
wantPeers: map[string][]string{
beforeDown: nil, afterDown: nil, afterSecondDown: {testPeerIP},
},
},
}
@@ -666,11 +671,16 @@ func TestPrefixHandlerRetriesAFailedDeleteOfAPeersRoutes(t *testing.T) {
h.HandleMessage(sessionDown())
h.HandleMessage(peerAnnouncement(testPeerIP, afterDown))
h.flushBatch() // the delete fails
for _, msg := range tt.afterFailing {
h.HandleMessage(msg)
}
h.flushBatch()
if store.deletes != tt.wantDeletes {
if store.deletes != wantDeletes {
t.Errorf("the store was asked for %d deletes of the peer's "+
"routes, want %d", store.deletes, tt.wantDeletes)
"routes, want %d", store.deletes, wantDeletes)
}
assertPeers(t, db, tt.wantPeers)