Reports refused while report files cannot be written (closes #123)
check / check (push) Successful in 2m4s
check / check (push) Successful in 2m4s
A failed write of a report file used to drop its reports, and POST /api/v1/reports kept answering 200 for as long as writes failed. Now a failed write puts its reports back in the buffer, still counted against DATA_DIR_MAX_BYTES, and Append refuses every report, which the handler answers 500, until a write succeeds. The minute flush tries the held reports again, so storing resumes with no restart once writing works, and the buffer takes nothing new while writes fail. Model: opus-5-5
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -17,6 +19,8 @@ import (
|
||||
|
||||
"sneak.berlin/go/netwatch/internal/config"
|
||||
"sneak.berlin/go/netwatch/internal/globals"
|
||||
"sneak.berlin/go/netwatch/internal/handlers"
|
||||
"sneak.berlin/go/netwatch/internal/healthcheck"
|
||||
"sneak.berlin/go/netwatch/internal/logger"
|
||||
"sneak.berlin/go/netwatch/internal/reportbuf"
|
||||
|
||||
@@ -507,14 +511,13 @@ func TestFileBeingWrittenIsNeverDeleted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFailedWriteStopsCounting makes a write fail once its file is
|
||||
// created. Its reports are lost, so they stop counting, and the part of
|
||||
// the file written is removed, so it takes no room.
|
||||
func TestFailedWriteStopsCounting(t *testing.T) {
|
||||
report := map[string]string{"id": "failed"}
|
||||
|
||||
t.Setenv("DATA_DIR", t.TempDir())
|
||||
t.Setenv("DATA_DIR_MAX_BYTES", strconv.Itoa(lineBytes(t, report)))
|
||||
// TestFailedWriteKeepsItsReports makes a write fail once its file is
|
||||
// created. The part of the file written is removed, so it takes no
|
||||
// room, and the report waits in memory for the next flush, which
|
||||
// writes it.
|
||||
func TestFailedWriteKeepsItsReports(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("DATA_DIR", dir)
|
||||
|
||||
buf := startBuffer(t)
|
||||
|
||||
@@ -527,9 +530,9 @@ func TestFailedWriteStopsCounting(t *testing.T) {
|
||||
_ = f.Close()
|
||||
})
|
||||
|
||||
err := buf.Append(report)
|
||||
err := buf.Append(map[string]string{"id": "kept"})
|
||||
if err != nil {
|
||||
t.Fatalf("report that fills the cap exactly: %v", err)
|
||||
t.Fatalf("append report: %v", err)
|
||||
}
|
||||
|
||||
err = buf.Flush()
|
||||
@@ -544,10 +547,135 @@ func TestFailedWriteStopsCounting(t *testing.T) {
|
||||
// Writes from here on, the final one at stop included, succeed.
|
||||
buf.OnFileCreated(func(*os.File) {})
|
||||
|
||||
err = buf.Append(report)
|
||||
err = buf.Flush()
|
||||
if err != nil {
|
||||
t.Fatalf("report after the failed write: %v", err)
|
||||
t.Fatalf("flush after the failed write: %v", err)
|
||||
}
|
||||
|
||||
files, err := readReportFiles(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("read report files: %v", err)
|
||||
}
|
||||
|
||||
want := `{"id":"kept"}` + "\n"
|
||||
if len(files) != 1 || files[0] != want {
|
||||
t.Fatalf("report files = %q, want one holding %q", files, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReportsRefusedWhileWritesFail posts reports through the report
|
||||
// handler, as the page does. Once a write has failed, a report is
|
||||
// refused with 500 until a write succeeds again; the reports taken
|
||||
// before and after are stored, and the one refused is not.
|
||||
func TestReportsRefusedWhileWritesFail(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("DATA_DIR", dir)
|
||||
|
||||
buf, h := startBufferWithHandlers(t)
|
||||
|
||||
if code := postReport(t, h, "before"); code != http.StatusOK {
|
||||
t.Fatalf("report before the failed write: status = %d, want %d",
|
||||
code, http.StatusOK)
|
||||
}
|
||||
|
||||
// With the data directory gone, the write has nowhere to go.
|
||||
err := os.Remove(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("remove data dir: %v", err)
|
||||
}
|
||||
|
||||
err = buf.Flush()
|
||||
if err == nil {
|
||||
t.Fatal("flush succeeded with the data directory gone")
|
||||
}
|
||||
|
||||
if code := postReport(t, h, "refused"); code != http.StatusInternalServerError {
|
||||
t.Fatalf("report after the failed write: status = %d, want %d",
|
||||
code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
err = os.Mkdir(dir, 0o750)
|
||||
if err != nil {
|
||||
t.Fatalf("make data dir: %v", err)
|
||||
}
|
||||
|
||||
err = buf.Flush()
|
||||
if err != nil {
|
||||
t.Fatalf("flush with the data directory back: %v", err)
|
||||
}
|
||||
|
||||
if code := postReport(t, h, "after"); code != http.StatusOK {
|
||||
t.Fatalf("report after a write succeeded: status = %d, want %d",
|
||||
code, http.StatusOK)
|
||||
}
|
||||
|
||||
err = buf.Flush()
|
||||
if err != nil {
|
||||
t.Fatalf("flush of the report after: %v", err)
|
||||
}
|
||||
|
||||
files, err := readReportFiles(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("read report files: %v", err)
|
||||
}
|
||||
|
||||
stored := strings.Join(files, "")
|
||||
|
||||
for _, clientID := range []string{"before", "after"} {
|
||||
if !strings.Contains(stored, `"clientId":"`+clientID+`"`) {
|
||||
t.Errorf("report %q not stored", clientID)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(stored, `"clientId":"refused"`) {
|
||||
t.Error("refused report stored")
|
||||
}
|
||||
}
|
||||
|
||||
// startBufferWithHandlers starts a Buffer and the Handlers that store
|
||||
// reports in it through fx, as main does, with the DATA_DIR the calling
|
||||
// test has set.
|
||||
func startBufferWithHandlers(
|
||||
t *testing.T,
|
||||
) (*reportbuf.Buffer, *handlers.Handlers) {
|
||||
t.Helper()
|
||||
|
||||
var (
|
||||
buf *reportbuf.Buffer
|
||||
h *handlers.Handlers
|
||||
)
|
||||
|
||||
app := fxtest.New(t,
|
||||
fx.Provide(
|
||||
config.New,
|
||||
globals.New,
|
||||
handlers.New,
|
||||
healthcheck.New,
|
||||
logger.New,
|
||||
reportbuf.New,
|
||||
),
|
||||
fx.Populate(&buf, &h),
|
||||
)
|
||||
|
||||
app.RequireStart()
|
||||
t.Cleanup(app.RequireStop)
|
||||
|
||||
return buf, h
|
||||
}
|
||||
|
||||
// postReport posts a report from clientID to the report handler, as the
|
||||
// page does, and returns the status of the answer.
|
||||
func postReport(t *testing.T, h *handlers.Handlers, clientID string) int {
|
||||
t.Helper()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequestWithContext(t.Context(),
|
||||
http.MethodPost, "/api/v1/reports",
|
||||
strings.NewReader(`{"clientId":"`+clientID+`","hosts":[]}`),
|
||||
)
|
||||
h.HandleReport().ServeHTTP(rec, req)
|
||||
|
||||
return rec.Code
|
||||
}
|
||||
|
||||
// TestConcurrentAppendsStopAtCap appends from many goroutines at once
|
||||
|
||||
Reference in New Issue
Block a user