fix deadlocks and add recovery middleware for http server errors
All checks were successful
pedestrian-simulator / build (push) Successful in 1m1s
All checks were successful
pedestrian-simulator / build (push) Successful in 1m1s
This commit is contained in:
@@ -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)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user