Feat: graceful shutdown

This commit is contained in:
Grail Finder
2025-06-15 09:51:25 +03:00
parent fa25679624
commit 30e322d9c6
6 changed files with 153 additions and 18 deletions

View File

@ -70,10 +70,11 @@ func getStateByCtx(ctx context.Context) (*models.UserState, error) {
} }
func saveFullInfo(fi *models.FullInfo) error { func saveFullInfo(fi *models.FullInfo) error {
// INFO: unfortunately working no transactions; so case are possible where first object is updated but the second is not // INFO: no transactions; so case is possible where first object is updated but the second is not
if err := saveState(fi.State.Username, fi.State); err != nil { if err := saveState(fi.State.Username, fi.State); err != nil {
return err return err
} }
log.Debug("saved user state", "state", fi.State)
if err := saveRoom(fi.Room); err != nil { if err := saveRoom(fi.Room); err != nil {
return err return err
} }

107
handlers/actions_test.go Normal file
View File

@ -0,0 +1,107 @@
package handlers
import (
"encoding/json"
"gralias/models"
"os"
"testing"
)
func TestSaveState(t *testing.T) {
// Create test state
state := &models.UserState{
Username: "testuser",
RoomID: "testroom",
Team: models.UserTeamBlue,
Role: models.UserRoleMime,
}
// Save state
err := saveState(state.Username, state)
if err != nil {
t.Fatalf("saveState failed: %v", err)
}
// Load state
loadedState, err := loadState(state.Username)
if err != nil {
t.Fatalf("loadState failed: %v", err)
}
// Verify loaded state matches original
if loadedState.Username != state.Username {
t.Errorf("Username mismatch: got %s, want %s", loadedState.Username, state.Username)
}
if loadedState.RoomID != state.RoomID {
t.Errorf("RoomID mismatch: got %s, want %s", loadedState.RoomID, state.RoomID)
}
if loadedState.Team != state.Team {
t.Errorf("Team mismatch: got %s, want %s", loadedState.Team, state.Team)
}
if loadedState.Role != state.Role {
t.Errorf("Role mismatch: got %s, want %s", loadedState.Role, state.Role)
}
// Test JSON serialization/deserialization
data, err := json.Marshal(state)
if err != nil {
t.Fatalf("marshal failed: %v", err)
}
testMap := make(map[string][]byte)
testMap["testkey"] = data
// Create a temporary file
tmpFile, err := os.CreateTemp("", "test_store_*.json")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
tmpFileName := tmpFile.Name()
// defer os.Remove(tmpFileName)
// Write testMap to the temp file
fileData, err := json.Marshal(testMap)
if err != nil {
t.Fatalf("failed to marshal testMap: %v", err)
}
if err := os.WriteFile(tmpFileName, fileData, 0644); err != nil {
t.Fatalf("failed to write to temp file: %v", err)
}
// Read the temp file
readData, err := os.ReadFile(tmpFileName)
if err != nil {
t.Fatalf("failed to read temp file: %v", err)
}
// Unmarshal the data
var testMapRead map[string][]byte
if err := json.Unmarshal(readData, &testMapRead); err != nil {
t.Fatalf("failed to unmarshal testMap: %v", err)
}
// Get the state bytes from the map
stateBytes, ok := testMapRead["testkey"]
if !ok {
t.Fatalf("key 'testkey' not found in testMapRead")
}
// Unmarshal the state bytes
stateRead := &models.UserState{}
if err := json.Unmarshal(stateBytes, stateRead); err != nil {
t.Fatalf("failed to unmarshal state: %v", err)
}
// Compare the state
if stateRead.Username != state.Username {
t.Errorf("Username mismatch from file: got %s, want %s", stateRead.Username, state.Username)
}
if stateRead.RoomID != state.RoomID {
t.Errorf("RoomID mismatch from file: got %s, want %s", stateRead.RoomID, state.RoomID)
}
if stateRead.Team != state.Team {
t.Errorf("Team mismatch from file: got %s, want %s", stateRead.Team, state.Team)
}
if stateRead.Role != state.Role {
t.Errorf("Role mismatch from file: got %s, want %s", stateRead.Role, state.Role)
}
}

View File

@ -9,6 +9,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"time"
) )
var ( var (
@ -26,6 +27,7 @@ func init() {
memcache = cache.MemCache memcache = cache.MemCache
cfg = config.LoadConfigOrDefault("") cfg = config.LoadConfigOrDefault("")
Notifier = broker.Notifier Notifier = broker.Notifier
cache.MemCache.StartBackupRoutine(15 * time.Second) // Reduced backup interval
} }
func HandlePing(w http.ResponseWriter, r *http.Request) { func HandlePing(w http.ResponseWriter, r *http.Request) {

30
main.go
View File

@ -1,10 +1,15 @@
package main package main
import ( import (
"context"
"gralias/config" "gralias/config"
"gralias/handlers" "gralias/handlers"
"gralias/pkg/cache"
"log/slog" "log/slog"
"net/http" "net/http"
"os"
"os/signal"
"syscall"
"time" "time"
) )
@ -14,7 +19,7 @@ func init() {
cfg = config.LoadConfigOrDefault("") cfg = config.LoadConfigOrDefault("")
} }
func ListenToRequests(port string) error { func ListenToRequests(port string) *http.Server {
mux := http.NewServeMux() mux := http.NewServeMux()
server := &http.Server{ server := &http.Server{
Handler: handlers.LogRequests(handlers.GetSession(mux)), Handler: handlers.LogRequests(handlers.GetSession(mux)),
@ -47,12 +52,27 @@ func ListenToRequests(port string) error {
// sse // sse
mux.Handle("GET /sub/sse", handlers.Notifier) mux.Handle("GET /sub/sse", handlers.Notifier)
slog.Info("Listening", "addr", port) slog.Info("Listening", "addr", port)
return server.ListenAndServe() return server
} }
func main() { func main() {
err := ListenToRequests(cfg.ServerConfig.Port) // Setup graceful shutdown
if err != nil { stop := make(chan os.Signal, 1)
panic(err) signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
server := ListenToRequests(cfg.ServerConfig.Port)
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
panic(err)
}
}()
<-stop
slog.Info("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
slog.Error("server shutdown failed", "error", err)
} }
cache.MemCache.BackupNow()
} }

26
pkg/cache/impl.go vendored
View File

@ -102,6 +102,19 @@ func (mc *MemoryCache) RemoveKey(key string) {
mc.lock.RUnlock() mc.lock.RUnlock()
} }
func (mc *MemoryCache) BackupNow() {
data := mc.GetAll()
jsonString, err := json.Marshal(data)
if err != nil {
slog.Warn("immediate backup failed to marshal", "err", err)
return
}
err = os.WriteFile(storeFileName, jsonString, os.ModePerm)
if err != nil {
slog.Warn("immediate backup failed to write", "err", err)
}
}
func (mc *MemoryCache) StartExpiryRoutine(n time.Duration) { func (mc *MemoryCache) StartExpiryRoutine(n time.Duration) {
ticker := time.NewTicker(n) ticker := time.NewTicker(n)
go func() { go func() {
@ -127,18 +140,7 @@ func (mc *MemoryCache) StartBackupRoutine(n time.Duration) {
go func() { go func() {
for { for {
<-ticker.C <-ticker.C
// get all mc.BackupNow()
data := mc.GetAll()
jsonString, err := json.Marshal(data)
if err != nil {
slog.Warn("failed to marshal", "err", err)
continue
}
err = os.WriteFile(storeFileName, jsonString, os.ModePerm)
if err != nil {
slog.Warn("failed to write", "err", err)
continue
}
} }
}() }()
} }

View File

@ -11,6 +11,8 @@
- show in backlog (and with that in prompt to llm) how many cards are left to open, also additional comment: if guess was right; - show in backlog (and with that in prompt to llm) how many cards are left to open, also additional comment: if guess was right;
- hide clue input for mime when it's not their turn; - hide clue input for mime when it's not their turn;
- needs resend to llm btn; + - needs resend to llm btn; +
- better styles and fluff;
- common auth system between sites;
#### sse points #### sse points
- clue sse update; - clue sse update;
@ -32,3 +34,4 @@
- gameover to backlog; - gameover to backlog;
- remove verbs from word file; - remove verbs from word file;
- invite link gets cutoff; - invite link gets cutoff;
- mime rejoined the room: does not see colors; state save in store.json has empty role and team