Skip to content

refactor for less mutexes on endconversation #37

Description

@Anthony-Bible

Refactoring Plan: Consolidate Session State Mutexes in ConversationService

Context

The ConversationService in internal/domain/service/conversation_service.go currently uses 8 separate mutexes to protect session-specific state maps:

Mutex State Map Purpose
conversationsMu conversations Conversation object storage
processingMu processing Tool execution state tracking
sessionModesMu sessionModes Plan mode state
sessionThinkingModesMu sessionThinkingModes Thinking mode configuration
sessionSystemPromptsMu sessionSystemPrompts Custom system prompts
sessionTokenUsageMu sessionTokenUsage Token usage for auto-compaction
sessionActiveSkillsMu sessionActiveSkills Active skills for tool restrictions
sessionAllowedToolsMu sessionAllowedTools Cached allowed tools union

The EndConversation method performs 16 lock/unlock operations (8 mutexes × 2 operations) to clean up a single session.

Problem Statement

  1. High Complexity: 8 separate mutexes make the code harder to reason about and maintain
  2. Performance Overhead: Each lock/unlock in EndConversation adds latency for a cleanup operation
  3. Bug Surface Area: Easy to forget to lock one of the mutexes when adding new session state
  4. Existing Inconsistency: Comment on line 41-43 notes: "Pre-existing methods (GetConversation, EndConversation, etc.) access conversations without this mutex. Consistent usage is a separate refactor."

Root Cause

As features were incrementally added to ConversationService, each new session-specific map was given its own mutex. This was done with good intentions (fine-grained locking), but it didn't consider that:

  1. All session state maps share the same key (sessionID)
  2. They are logically related (all describe metadata about a session)
  3. Most access patterns already require two locks anyway (check session exists → access state)
  4. Cleanup operations like EndConversation must access all maps anyway

Proposed Solution

Consolidate all session-specific state into a single SessionState struct protected by one mutex.

Design

// SessionState groups all session-specific metadata under a single mutex
type SessionState struct {
    mu                sync.RWMutex
    processing        bool
    planMode          bool
    thinkingMode      port.ThinkingModeInfo
    systemPrompt      string
    tokenUsage        int64
    activeSkills      []entity.Skill
    allowedTools      map[string]bool
    // Note: conversation entity remains separate for non-session-specific operations
}

// ConversationService struct changes
type ConversationService struct {
    aiProvider        port.AIProvider
    toolExecutor      port.ToolExecutor
    conversations     map[string]*entity.Conversation
    conversationsMu   sync.RWMutex // Protects conversations map
    sessionStates     map[string]*SessionState // New: single session state map
    currentSession    string
    compactionThreshold int64
}

Benefits

  1. Single lock for session meta operations: EndConversation now only needs 1-2 locks instead of 8
  2. Simpler mental model: All session metadata in one place
  3. Reduced bug surface: Only one mutex to remember when adding session state
  4. Consistent locking patterns: All session operations follow same pattern
  5. Better for atomic operations: Related state changes can be atomic

Trade-offs

  1. Increased lock contention: If many goroutines access different session metadata, they might wait on the same lock
  2. More complex individual operations: Reading/writing state requires an extra level of indirection
  3. Breaking changes: All methods accessing session state must be updated

Risk Assessment: Low to Medium

  • The codebase has tests (conversation_service_test.go) to verify functionality
  • Session operations are not extremely hot paths (cleanup, configuration changes)
  • Most session maps already require two locks (existence check + data access)
  • RWMutex behavior can be preserved for read-heavy operations

Implementation Steps

Step 1: Create SessionState Struct

  • Add SessionState struct with all session-related fields
  • Add sessionStates map to ConversationService

Step 2: Update StartConversation

  • Initialize SessionState for new sessions
  • Keep conversations separate (not moved into SessionState)

Step 3: Update EndConversation

  • Simplify to only lock sessionStates[sessionID].mu
  • Delete entire SessionState entry

Step 4: Update Setter Methods

  • Update these methods to use sessionStates[sessionID].mu:
    • SetPlanMode()
    • SetThinkingMode()
    • SetCustomSystemPrompt()
    • SetProcessingState()
    • AppendActiveSkill(), SetActiveSkills(), RemoveActiveSkill()
    • Internal methods: setTokenUsage(), etc.

Step 5: Update Getter Methods

  • Update these methods to use sessionStates[sessionID].mu (RLock):
    • IsPlanMode()
    • GetThinkingMode()
    • GetCustomSystemPrompt()
    • IsProcessing()
    • GetActiveSkills()
    • GetAllowedToolsForSession()
    • GetTokenUsage()

Step 6: Update Helper Methods

  • computeAllowedTools() - no changes needed
  • Methods that set sessionAllowedToolsMu must now get the session state first

Step 7: Remove Old Mutexes

  • Remove 7 mutexes (keep conversationsMu)
  • Remove their corresponding maps
  • Keep processing, sessionModes, etc. as struct fields in SessionState

Step 8: Run Tests

  • Run full test suite: go test ./internal/domain/service/...
  • Verify all existing tests pass

Step 9: Code Review

  • Ensure all mutex accesses are correct
  • Check for potential deadlocks
  • Verify RWMutex usage is appropriate

Files to Modify

  1. internal/domain/service/conversation_service.go
    • Add SessionState struct
    • Modify ConversationService struct
    • Update all methods that access session state

Testing Plan

  1. Run existing unit tests to ensure no regression
  2. Verify thread safety through concurrent test scenarios
  3. Performance test to ensure no meaningful degradation
  4. Check for data races with -race flag

Rollback Strategy

If issues arise:

  1. The refactor can be reverted by restoring the old structure
  2. Git history makes rollback straightforward
  3. Consider using a feature flag if gradual rollout is desired

Success Criteria

  1. EndConversation reduced from 16 to 2 lock/unlock operations
  2. ✅ All existing tests pass
  3. ✅ No race conditions detected with -race flag
  4. ✅ Code is simpler and easier to understand
  5. ✅ Performance is not meaningfully degraded

Estimated Effort: 2-3 hours
Risk Level: Low-Medium
Priority: Medium (code quality improvement, not blocking feature work)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions