feat(sessions): edit session heartbeat and stale session sweeper #164

Open
opened 2026-03-01 15:37:37 +00:00 by forbes · 0 comments
Owner

Context

Sub-issue of #125 (Context-Aware Part Subscription System).

Edit sessions must be kept alive by the client's SSE connection. If a client crashes, loses network, or the user walks away, the session becomes stale and must be auto-released to avoid orphaned locks.

Heartbeat Mechanism

The SSE connection is the heartbeat signal. While a workstation has an active SSE connection, its sessions are alive.

1. SSE Heartbeat Integration

In the SSE handler, when the broker heartbeat fires (every 30s), update all edit sessions for connected workstations:

// In broker heartbeat goroutine or SSE handler:
func (s *Server) touchWorkstationSessions(workstationID string) {
    s.editSessions.TouchHeartbeat(ctx, workstationID)
}

The TouchHeartbeat method (from #163) runs:

UPDATE edit_sessions SET last_heartbeat = now() WHERE workstation_id = $1

2. Stale Session Sweeper

Add a background goroutine started in cmd/silod/main.go:

func (s *Server) startSessionSweeper(interval, timeout time.Duration) {
    ticker := time.NewTicker(interval)
    for {
        select {
        case <-ticker.C:
            released, err := s.editSessions.ExpireStale(ctx, timeout)
            for _, session := range released {
                s.broker.PublishToItem(session.ItemID, "edit.session_released", ...)
                s.logger.Info().Str("session", session.ID).Str("user", session.UserID).Msg("auto-released stale session")
            }
        case <-s.done:
            return
        }
    }
}
  • Sweep interval: 30 seconds
  • Timeout: session_timeout_minutes from config (default 15 min)
  • ExpireStale() returns the list of released sessions so SSE events can be broadcast

3. SSE Disconnect Cleanup

When an SSE connection closes (client disconnect), release all sessions for that workstation:

// In HandleEvents, on connection close:
defer func() {
    s.broker.Unsubscribe(client)
    if wsID != "" {
        released, _ := s.editSessions.ReleaseForWorkstation(ctx, wsID)
        for _, session := range released {
            s.broker.PublishToItem(session.ItemID, "edit.session_released", ...)
        }
    }
}()

This provides immediate cleanup rather than waiting for the sweeper.

4. Config

Uses the session_timeout_minutes field from the sessions module config (#161):

modules:
  sessions:
    session_timeout_minutes: 15   # auto-release after this many minutes without heartbeat

Files to Modify

  • internal/api/sse_handler.go — disconnect cleanup, heartbeat touch
  • cmd/silod/main.go — start sweeper goroutine
  • internal/api/session_handlers.go or new file — sweeper logic

Acceptance Criteria

  • SSE heartbeat updates last_heartbeat for workstation's sessions
  • Sweeper runs every 30s, releases sessions older than session_timeout_minutes
  • Released sessions emit edit.session_released SSE events
  • SSE disconnect immediately releases all sessions for that workstation
  • Sweeper logs released sessions at INFO level
  • Sweeper gracefully stops on server shutdown

Depends On

  • #163 (edit session table and endpoints)

Part Of

#125

## Context Sub-issue of #125 (Context-Aware Part Subscription System). Edit sessions must be kept alive by the client's SSE connection. If a client crashes, loses network, or the user walks away, the session becomes stale and must be auto-released to avoid orphaned locks. ## Heartbeat Mechanism The SSE connection is the heartbeat signal. While a workstation has an active SSE connection, its sessions are alive. ### 1. SSE Heartbeat Integration In the SSE handler, when the broker heartbeat fires (every 30s), update all edit sessions for connected workstations: ```go // In broker heartbeat goroutine or SSE handler: func (s *Server) touchWorkstationSessions(workstationID string) { s.editSessions.TouchHeartbeat(ctx, workstationID) } ``` The `TouchHeartbeat` method (from #163) runs: ```sql UPDATE edit_sessions SET last_heartbeat = now() WHERE workstation_id = $1 ``` ### 2. Stale Session Sweeper Add a background goroutine started in `cmd/silod/main.go`: ```go func (s *Server) startSessionSweeper(interval, timeout time.Duration) { ticker := time.NewTicker(interval) for { select { case <-ticker.C: released, err := s.editSessions.ExpireStale(ctx, timeout) for _, session := range released { s.broker.PublishToItem(session.ItemID, "edit.session_released", ...) s.logger.Info().Str("session", session.ID).Str("user", session.UserID).Msg("auto-released stale session") } case <-s.done: return } } } ``` - **Sweep interval:** 30 seconds - **Timeout:** `session_timeout_minutes` from config (default 15 min) - `ExpireStale()` returns the list of released sessions so SSE events can be broadcast ### 3. SSE Disconnect Cleanup When an SSE connection closes (client disconnect), release all sessions for that workstation: ```go // In HandleEvents, on connection close: defer func() { s.broker.Unsubscribe(client) if wsID != "" { released, _ := s.editSessions.ReleaseForWorkstation(ctx, wsID) for _, session := range released { s.broker.PublishToItem(session.ItemID, "edit.session_released", ...) } } }() ``` This provides immediate cleanup rather than waiting for the sweeper. ### 4. Config Uses the `session_timeout_minutes` field from the sessions module config (#161): ```yaml modules: sessions: session_timeout_minutes: 15 # auto-release after this many minutes without heartbeat ``` ## Files to Modify - `internal/api/sse_handler.go` — disconnect cleanup, heartbeat touch - `cmd/silod/main.go` — start sweeper goroutine - `internal/api/session_handlers.go` or new file — sweeper logic ## Acceptance Criteria - [ ] SSE heartbeat updates `last_heartbeat` for workstation's sessions - [ ] Sweeper runs every 30s, releases sessions older than `session_timeout_minutes` - [ ] Released sessions emit `edit.session_released` SSE events - [ ] SSE disconnect immediately releases all sessions for that workstation - [ ] Sweeper logs released sessions at INFO level - [ ] Sweeper gracefully stops on server shutdown ## Depends On - #163 (edit session table and endpoints) ## Part Of #125
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/silo#164