Feat: graceful shutdown
This commit is contained in:
@ -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
107
handlers/actions_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
28
main.go
28
main.go
@ -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)
|
||||||
|
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)
|
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
26
pkg/cache/impl.go
vendored
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
3
todos.md
3
todos.md
@ -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
|
||||||
|
Reference in New Issue
Block a user