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
	 Grail Finder
					Grail Finder