fix(server): stop the background compaction before the database is closed
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m35s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m35s
The compaction starts right away in NewServer and could still write to the SQLite files while a
test's temp dir was removed ("directory not empty" on the CI runner, TestWebhookTokenInURLOnlyAPIKey).
Server.Close stops it and waits for a running pass; tests call it before closing the store, and the
server calls it on shutdown before the deferred store.Close. v0.37.1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3fbe015c06
commit
c56bd4d943
10 files changed
+43
-6
No files matched your search
+7
-1
@@ -6,6 +6,11 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.37.1] - 2026-11-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Beim Herunterfahren wartet der Server, bis eine laufende Verdichtung des Notiz-Protokolls fertig ist, bevor er die Datenbank schließt. Dieselbe Lücke ließ einen Test auf dem Build-Server gelegentlich scheitern.
|
||||||
|
|
||||||
## [0.37.0] - 2026-11-01
|
## [0.37.0] - 2026-11-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -617,7 +622,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
- Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken.
|
- Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken.
|
||||||
- Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung.
|
- Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung.
|
||||||
|
|
||||||
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.37.0...dev
|
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.37.1...dev
|
||||||
|
[0.37.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.37.0...v0.37.1
|
||||||
[0.37.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.36.0...v0.37.0
|
[0.37.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.36.0...v0.37.0
|
||||||
[0.36.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.35.0...v0.36.0
|
[0.36.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.35.0...v0.36.0
|
||||||
[0.35.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.34.0...v0.35.0
|
[0.35.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.34.0...v0.35.0
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
[](#)
|
[](#)
|
||||||
[](#-schnellstart)
|
[](#-schnellstart)
|
||||||
[](#-deine-daten-gehören-dir)
|
[](#-deine-daten-gehören-dir)
|
||||||
[](CHANGELOG.md)
|
[](CHANGELOG.md)
|
||||||
|
|
||||||
**MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server.
|
**MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server.
|
||||||
|
|
||||||
|
|||||||
@@ -103,4 +103,6 @@ func main() {
|
|||||||
if err := httpServer.Shutdown(ctx); err != nil {
|
if err := httpServer.Shutdown(ctx); err != nil {
|
||||||
slog.Warn("HTTP server shutdown finished with error", "error", err)
|
slog.Warn("HTTP server shutdown finished with error", "error", err)
|
||||||
}
|
}
|
||||||
|
// Let a running compaction finish before the deferred store.Close
|
||||||
|
srv.Close()
|
||||||
}
|
}
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.37.0",
|
"version": "0.37.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.37.0",
|
"version": "0.37.1",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "5.3.0",
|
"@fontsource-variable/inter": "5.3.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.37.0",
|
"version": "0.37.1",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func TestDemoResetKeepsSessionsAndRestoresSampleData(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
srv := NewServer(&config.Config{DatabasePath: dbPath, JWTSecret: "demo-secret", DemoMode: true}, store, ws.NewHub(), nil)
|
srv := NewServer(&config.Config{DatabasePath: dbPath, JWTSecret: "demo-secret", DemoMode: true}, store, ws.NewHub(), nil)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
router := srv.Router()
|
router := srv.Router()
|
||||||
call := func(method, path, token, body string) *httptest.ResponseRecorder {
|
call := func(method, path, token, body string) *httptest.ResponseRecorder {
|
||||||
req := httptest.NewRequest(method, path, strings.NewReader(body))
|
req := httptest.NewRequest(method, path, strings.NewReader(body))
|
||||||
|
|||||||
@@ -266,9 +266,14 @@ func (s *Server) handleDocLeave(w http.ResponseWriter, r *http.Request) {
|
|||||||
const compactionInterval = 6 * time.Hour
|
const compactionInterval = 6 * time.Hour
|
||||||
|
|
||||||
func (s *Server) startCompaction() {
|
func (s *Server) startCompaction() {
|
||||||
|
defer s.bg.Done()
|
||||||
for {
|
for {
|
||||||
s.compactOnce()
|
s.compactOnce()
|
||||||
time.Sleep(compactionInterval)
|
select {
|
||||||
|
case <-s.stop:
|
||||||
|
return
|
||||||
|
case <-time.After(compactionInterval):
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func TestDemoSendsNothingAndKeepsAccounts(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
srv := NewServer(&config.Config{DatabasePath: dbPath, JWTSecret: "demo-secret", DemoMode: true}, store, ws.NewHub(), nil)
|
srv := NewServer(&config.Config{DatabasePath: dbPath, JWTSecret: "demo-secret", DemoMode: true}, store, ws.NewHub(), nil)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
router := srv.Router()
|
router := srv.Router()
|
||||||
token := sessionLogin(t, router, "admin", "admin", "visitor-1")
|
token := sessionLogin(t, router, "admin", "admin", "visitor-1")
|
||||||
_ = sessionLogin(t, router, "admin", "admin", "visitor-2")
|
_ = sessionLogin(t, router, "admin", "admin", "visitor-2")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
@@ -41,6 +42,11 @@ type Server struct {
|
|||||||
notifyPushHook func(userID string, msg pushMessage) error
|
notifyPushHook func(userID string, msg pushMessage) error
|
||||||
pushHTTPClient *http.Client // tests: reach a local push service
|
pushHTTPClient *http.Client // tests: reach a local push service
|
||||||
push pushKeys
|
push pushKeys
|
||||||
|
|
||||||
|
// Background work that touches the database right from the start (compaction); Close stops it
|
||||||
|
stop chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
|
bg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS fs.FS) *Server {
|
func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS fs.FS) *Server {
|
||||||
@@ -51,6 +57,7 @@ func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS
|
|||||||
staticFS: staticFS,
|
staticFS: staticFS,
|
||||||
leases: collab.NewLeaseManager(nil),
|
leases: collab.NewLeaseManager(nil),
|
||||||
notifier: collab.NewNotifier(),
|
notifier: collab.NewNotifier(),
|
||||||
|
stop: make(chan struct{}),
|
||||||
}
|
}
|
||||||
srv.loginGuard = newLoginGuard(!cfg.DisableLoginProtection)
|
srv.loginGuard = newLoginGuard(!cfg.DisableLoginProtection)
|
||||||
srv.voice = newVoiceService(cfg.Speech.MaxParallel)
|
srv.voice = newVoiceService(cfg.Speech.MaxParallel)
|
||||||
@@ -63,6 +70,7 @@ func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS
|
|||||||
store.OnTaskCreated = func(th *domain.Thought) {
|
store.OnTaskCreated = func(th *domain.Thought) {
|
||||||
srv.wsHub.BroadcastToUsers(srv.taskAudience(th), "TASK_CREATED", th)
|
srv.wsHub.BroadcastToUsers(srv.taskAudience(th), "TASK_CREATED", th)
|
||||||
}
|
}
|
||||||
|
srv.bg.Add(1)
|
||||||
go srv.startCompaction()
|
go srv.startCompaction()
|
||||||
if cfg.DemoMode && cfg.DemoResetMinutes > 0 {
|
if cfg.DemoMode && cfg.DemoResetMinutes > 0 {
|
||||||
go srv.startDemoResetTicker()
|
go srv.startDemoResetTicker()
|
||||||
@@ -93,6 +101,13 @@ func (s *Server) taskAudience(th *domain.Thought) []string {
|
|||||||
return append(s.store.GetTaskListUserIDs(th.TaskListID), th.UserID)
|
return append(s.store.GetTaskListUserIDs(th.TaskListID), th.UserID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close stops the background compaction and waits until a running pass has finished, so the database
|
||||||
|
// can be closed safely afterwards (tests close it right away; a pass still writing would race with that).
|
||||||
|
func (s *Server) Close() {
|
||||||
|
s.stopOnce.Do(func() { close(s.stop) })
|
||||||
|
s.bg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) Router() http.Handler {
|
func (s *Server) Router() http.Handler {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ func setupTestServer(t *testing.T) (*Server, *storage.Store, string) {
|
|||||||
}
|
}
|
||||||
hub := ws.NewHub()
|
hub := ws.NewHub()
|
||||||
srv := NewServer(cfg, store, hub, nil)
|
srv := NewServer(cfg, store, hub, nil)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
token, err := auth.GenerateToken(cfg.JWTSecret, user.ID, user.Email, user.DisplayName, user.TokenVersion, testSession(t, store, user.ID))
|
token, err := auth.GenerateToken(cfg.JWTSecret, user.ID, user.Email, user.DisplayName, user.TokenVersion, testSession(t, store, user.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -71,6 +72,7 @@ func TestFirstBootSetupAndInviteRegistrationAndNonNullSlices(t *testing.T) {
|
|||||||
AllowRegistration: false,
|
AllowRegistration: false,
|
||||||
}
|
}
|
||||||
srv := NewServer(cfg, store, ws.NewHub(), nil)
|
srv := NewServer(cfg, store, ws.NewHub(), nil)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
router := srv.Router()
|
router := srv.Router()
|
||||||
|
|
||||||
// 1. Fresh DB must report needsSetup = true
|
// 1. Fresh DB must report needsSetup = true
|
||||||
@@ -455,6 +457,7 @@ func TestLoginWithUsernameAndDemoMode(t *testing.T) {
|
|||||||
DemoMode: true,
|
DemoMode: true,
|
||||||
}
|
}
|
||||||
srv := NewServer(cfg, store, ws.NewHub(), nil)
|
srv := NewServer(cfg, store, ws.NewHub(), nil)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
router := srv.Router()
|
router := srv.Router()
|
||||||
|
|
||||||
// 1. Check auth status reports demoMode: true
|
// 1. Check auth status reports demoMode: true
|
||||||
@@ -542,6 +545,7 @@ func TestSpaceMembersDirectInvitationAPI(t *testing.T) {
|
|||||||
JWTSecret: "test-secret",
|
JWTSecret: "test-secret",
|
||||||
}
|
}
|
||||||
srv := NewServer(cfg, store, ws.NewHub(), nil)
|
srv := NewServer(cfg, store, ws.NewHub(), nil)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
router := srv.Router()
|
router := srv.Router()
|
||||||
|
|
||||||
adminUser, err := store.GetUserByLogin("admin")
|
adminUser, err := store.GetUserByLogin("admin")
|
||||||
@@ -771,6 +775,7 @@ func TestWebhookTriageBadgeAndIngestion(t *testing.T) {
|
|||||||
AllowRegistration: false,
|
AllowRegistration: false,
|
||||||
}
|
}
|
||||||
srv := NewServer(cfg, store, ws.NewHub(), nil)
|
srv := NewServer(cfg, store, ws.NewHub(), nil)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
router := srv.Router()
|
router := srv.Router()
|
||||||
|
|
||||||
// 1. Setup user
|
// 1. Setup user
|
||||||
@@ -864,6 +869,7 @@ func TestSSEGlobalEndpoint(t *testing.T) {
|
|||||||
AllowRegistration: false,
|
AllowRegistration: false,
|
||||||
}
|
}
|
||||||
srv := NewServer(cfg, store, hub, nil)
|
srv := NewServer(cfg, store, hub, nil)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
router := srv.Router()
|
router := srv.Router()
|
||||||
|
|
||||||
setupBody := []byte(`{
|
setupBody := []byte(`{
|
||||||
@@ -1345,6 +1351,7 @@ func TestFirstBootSetupWithSampleData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer store.Close()
|
defer store.Close()
|
||||||
srv := NewServer(&config.Config{Port: "8080", DatabasePath: dbPath, JWTSecret: "test-jwt-secret-key"}, store, ws.NewHub(), nil)
|
srv := NewServer(&config.Config{Port: "8080", DatabasePath: dbPath, JWTSecret: "test-jwt-secret-key"}, store, ws.NewHub(), nil)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
// Too short passwords are refused (FLAW-09)
|
// Too short passwords are refused (FLAW-09)
|
||||||
short := httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", strings.NewReader(`{"email":"me@example.com","password":"secret"}`))
|
short := httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", strings.NewReader(`{"email":"me@example.com","password":"secret"}`))
|
||||||
|
|||||||
Reference in new issue
Block a user