fix deadlocks and add recovery middleware for http server errors
All checks were successful
pedestrian-simulator / build (push) Successful in 1m1s

This commit is contained in:
2026-01-11 20:34:06 -07:00
parent 512b36b10e
commit 2286422503
2 changed files with 59 additions and 14 deletions

View File

@@ -11,6 +11,19 @@ import (
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
) )
// RecoveryMiddleware catches panics and returns 500
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// getStepManager creates a new StepManager for the given user, loading state from DB // getStepManager creates a new StepManager for the given user, loading state from DB
func getStepManager(userID string) *StepManager { func getStepManager(userID string) *StepManager {
return NewStepManager(userID) return NewStepManager(userID)
@@ -125,5 +138,5 @@ func main() {
// 6. Start Server // 6. Start Server
binding := "0.0.0.0:8080" binding := "0.0.0.0:8080"
fmt.Printf("Server starting on http://%s\n", binding) fmt.Printf("Server starting on http://%s\n", binding)
log.Fatal(http.ListenAndServe(binding, nil)) log.Fatal(http.ListenAndServe(binding, RecoveryMiddleware(http.DefaultServeMux)))
} }

View File

@@ -28,15 +28,24 @@ type StepManager struct {
} }
func NewStepManager(userID string) *StepManager { func NewStepManager(userID string) *StepManager {
now := time.Now()
sm := &StepManager{ sm := &StepManager{
userID: userID, userID: userID,
syncInterval: 15 * time.Minute, syncInterval: 15 * time.Minute,
lastSyncTime: now.Add(-15 * time.Minute),
nextSyncTime: now.Add(24 * time.Hour), // Don't sync unless a trip is started or loaded
} }
sm.LoadTripState() sm.LoadTripState()
return sm return sm
} }
func (sm *StepManager) LoadTripState() error { func (sm *StepManager) LoadTripState() error {
sm.mu.Lock()
defer sm.mu.Unlock()
return sm.loadTripStateLocked()
}
func (sm *StepManager) loadTripStateLocked() error {
var startTime time.Time var startTime time.Time
err := db.QueryRow(` err := db.QueryRow(`
SELECT start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time SELECT start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time
@@ -47,7 +56,11 @@ func (sm *StepManager) LoadTripState() error {
) )
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil // Normal for first run // Initialize with defaults if no trip exists
now := time.Now()
sm.tripState.StartDate = "" // No active trip
sm.nextSyncTime = now.Add(24 * time.Hour)
return nil
} }
return err return err
} }
@@ -74,6 +87,12 @@ func (sm *StepManager) LoadTripState() error {
} }
func (sm *StepManager) SaveTripState() { func (sm *StepManager) SaveTripState() {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.saveTripStateLocked()
}
func (sm *StepManager) saveTripStateLocked() {
_, err := db.Exec(` _, err := db.Exec(`
INSERT INTO trips (user_id, start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time) INSERT INTO trips (user_id, start_date, start_time, start_day_initial_steps, previous_total_steps, target_total_steps, last_sync_time, next_sync_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
@@ -122,7 +141,7 @@ func (sm *StepManager) StartNewTrip() {
// On new trip, previous total is 0 // On new trip, previous total is 0
sm.previousTotalSteps = 0 sm.previousTotalSteps = 0
sm.targetTotalSteps = 0 sm.targetTotalSteps = 0
sm.SaveTripState() sm.saveTripStateLocked()
// Trigger immediate sync to set baseline // Trigger immediate sync to set baseline
go sm.Sync() go sm.Sync()
@@ -153,7 +172,19 @@ func (sm *StepManager) performSync(interval time.Duration) {
today := time.Now().Format("2006-01-02") today := time.Now().Format("2006-01-02")
// Parse start date in local time // Parse start date in local time
start, _ := time.ParseInLocation("2006-01-02", tripStateCopy.StartDate, time.Local) if tripStateCopy.StartDate == "" {
fmt.Println("[StepManager] Skipping sync: No active trip start date")
sm.mu.Lock()
sm.nextSyncTime = time.Now().Add(1 * time.Hour) // Check again in an hour
sm.mu.Unlock()
return
}
start, err := time.ParseInLocation("2006-01-02", tripStateCopy.StartDate, time.Local)
if err != nil {
fmt.Printf("[StepManager] ERROR: Invalid start date '%s': %v\n", tripStateCopy.StartDate, err)
return
}
end, _ := time.ParseInLocation("2006-01-02", today, time.Local) end, _ := time.ParseInLocation("2006-01-02", today, time.Local)
// Iterate from Start Date to Today // Iterate from Start Date to Today
@@ -204,7 +235,7 @@ func (sm *StepManager) performSync(interval time.Duration) {
// Update State // Update State
sm.tripState.DailyCache = newDailyCache sm.tripState.DailyCache = newDailyCache
sm.SaveTripState() sm.saveTripStateLocked()
// Update Smoothing Targets // Update Smoothing Targets
sm.previousTotalSteps = sm.calculateSmoothedTokenAt(time.Now()) // Snapshot current interpolated value as new start sm.previousTotalSteps = sm.calculateSmoothedTokenAt(time.Now()) // Snapshot current interpolated value as new start
@@ -239,22 +270,23 @@ func (sm *StepManager) calculateSmoothedTokenAt(t time.Time) int {
} }
func (sm *StepManager) GetStatus() map[string]interface{} { func (sm *StepManager) GetStatus() map[string]interface{} {
sm.mu.Lock()
defer sm.mu.Unlock()
// Reload from DB to get latest sync results from other instances // Reload from DB to get latest sync results from other instances
sm.LoadTripState() sm.LoadTripState()
// Auto-trigger sync if needed sm.mu.Lock()
if time.Now().After(sm.nextSyncTime) { shouldSync := time.Now().After(sm.nextSyncTime)
sm.Sync() // Sync and save to DB nextSyncMilli := sm.nextSyncTime.UnixMilli()
}
currentSmoothed := sm.calculateSmoothedTokenAt(time.Now()) currentSmoothed := sm.calculateSmoothedTokenAt(time.Now())
sm.mu.Unlock()
// Auto-trigger sync if needed (out of initial lock to avoid deadlock)
if shouldSync {
go sm.Sync() // Async sync
}
return map[string]interface{}{ return map[string]interface{}{
"tripSteps": currentSmoothed, "tripSteps": currentSmoothed,
"nextSyncTime": sm.nextSyncTime.UnixMilli(), "nextSyncTime": nextSyncMilli,
} }
} }