Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions internal/server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"log/slog"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -350,17 +351,34 @@ func (r *Router) saveStateSnapshot() error {
return nil
})

f, err := os.Create(r.statePath)
tmp, err := os.CreateTemp(filepath.Dir(r.statePath), ".kamal-proxy.state.*")
if err != nil {
slog.Error("Unable to create temp state file", "error", err)
return err
}
defer os.Remove(tmp.Name()) // clean up on any failure path

err = json.NewEncoder(f).Encode(services)
err = json.NewEncoder(tmp).Encode(services)
if err != nil {
tmp.Close()
slog.Error("Unable to save state", "error", err, "path", r.statePath)
return err
}

err = tmp.Sync()
if err != nil {
tmp.Close()
slog.Error("Unable to sync state file", "error", err)
return err
}
tmp.Close()

err = os.Rename(tmp.Name(), r.statePath)
if err != nil {
slog.Error("Unable to rename state file", "error", err)
return err
}

slog.Debug("Saved state", "path", r.statePath)
return nil
}
Expand Down
26 changes: 26 additions & 0 deletions internal/server/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,32 @@ func testRouter(t *testing.T) *Router {
return NewRouter(statePath)
}

func TestRouter_StateFileSurvivesRestart(t *testing.T) {
statePath := filepath.Join(t.TempDir(), "state.json")
_, target := testBackend(t, "first", http.StatusOK)

router := NewRouter(statePath)
require.NoError(t, router.DeployService("service1", []string{target},
defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions))

// Verify state file exists and is valid JSON
f, err := os.Open(statePath)
require.NoError(t, err)
defer f.Close()

var services []*Service
require.NoError(t, json.NewDecoder(f).Decode(&services))
assert.Len(t, services, 1)

// Verify no temp files left behind
entries, err := os.ReadDir(filepath.Dir(statePath))
require.NoError(t, err)
for _, entry := range entries {
assert.False(t, strings.HasPrefix(entry.Name(), ".kamal-proxy.state."),
"temp file should not remain: %s", entry.Name())
}
}

func sendGETRequest(router *Router, url string) (int, string) {
req := httptest.NewRequest(http.MethodGet, url, nil)
return sendRequest(router, req)
Expand Down