fix: reduce observer promotion delay from ~40s to ~11s

1. Terminal access permission check:
   - Add Permission.TERMINAL_ACCESS check to Web Terminal button
   - Prevents observer sessions from accessing terminal

2. Immediate websocket cleanup:
   - Close peer connection immediately when websocket errors
   - Previously waited 24+ seconds for ICE to transition from disconnected to failed
   - Now triggers session cleanup immediately on tab close

3. Immediate grace period validation:
   - Trigger validateSinglePrimary() immediately when grace period expires
   - Previously waited up to 10 seconds for next periodic validation
   - Eliminates unnecessary delay in observer promotion

Timeline improvement:
Before: Tab close → 6s (ICE disconnect) → 24s (ICE fail) → RemoveSession → 10s grace → up to 10s validation = ~50s total
After: Tab close → immediate peerConnection.Close() → immediate RemoveSession → 10s grace → immediate validation = ~11s total
This commit is contained in:
Alex P 2025-10-09 11:39:00 +03:00
parent 7901677551
commit b388bc3c62
3 changed files with 21 additions and 6 deletions

View File

@ -1520,9 +1520,11 @@ func (sm *SessionManager) cleanupInactiveSessions(ctx context.Context) {
needsBroadcast := false
// Check for expired grace periods and promote if needed
gracePeriodExpired := false
for sessionID, graceTime := range sm.reconnectGrace {
if now.After(graceTime) {
delete(sm.reconnectGrace, sessionID)
gracePeriodExpired = true
wasHoldingPrimarySlot := (sm.lastPrimaryID == sessionID)
@ -1735,12 +1737,18 @@ func (sm *SessionManager) cleanupInactiveSessions(ctx context.Context) {
}
}
// Periodic validateSinglePrimary to catch deadlock states
validationCounter++
if validationCounter >= 10 { // Every 10 seconds
validationCounter = 0
sm.logger.Debug().Msg("Running periodic session validation to catch deadlock states")
// Run validation immediately if a grace period expired, otherwise run periodically
if gracePeriodExpired {
sm.logger.Debug().Msg("Running immediate validation after grace period expiration")
sm.validateSinglePrimary()
} else {
// Periodic validateSinglePrimary to catch deadlock states
validationCounter++
if validationCounter >= 10 { // Every 10 seconds
validationCounter = 0
sm.logger.Debug().Msg("Running periodic session validation to catch deadlock states")
sm.validateSinglePrimary()
}
}
sm.mu.Unlock()

View File

@ -93,7 +93,7 @@ export default function Actionbar({
className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 py-1.5"
>
<div className="relative flex flex-wrap items-center gap-x-2 gap-y-2">
{developerMode && (
{developerMode && hasPermission(Permission.TERMINAL_ACCESS) && (
<Button
size="XS"
theme="light"

7
web.go
View File

@ -357,6 +357,13 @@ func handleWebRTCSignalWsMessages(
typ, msg, err := wsCon.Read(runCtx)
if err != nil {
l.Warn().Str("error", err.Error()).Msg("websocket read error")
// Clean up session when websocket closes
if session := sessionManager.GetSession(connectionID); session != nil && session.peerConnection != nil {
l.Info().
Str("sessionID", session.ID).
Msg("Closing peer connection due to websocket error")
_ = session.peerConnection.Close()
}
return err
}
if typ != websocket.MessageText {