Skip to content

Conversation

JAORMX
Copy link
Collaborator

@JAORMX JAORMX commented Sep 21, 2025

Overview

Refactors the session management system to use a pluggable storage interface, enabling future support for distributed storage backends like Redis/Valkey while maintaining backward compatibility.

What Changed

Core Architecture

  • Introduced a Storage interface that abstracts session persistence operations
  • Refactored Manager to use the Storage interface instead of directly using sync.Map
  • Created LocalStorage implementation that maintains the existing in-memory behavior
  • Added JSON serialization support for sessions to enable future network storage

Session Interface Enhancements

  • Extended Session interface with Type() and metadata methods that were already implemented in concrete types
  • Added GetData(), SetData(), GetMetadata(), and SetMetadata() methods for better data management

New Components

  • Storage interface with methods: Store(), Load(), Delete(), DeleteExpired(), Close()
  • LocalStorage implementation using sync.Map for backward compatibility
  • SessionType enum for better type identification
  • JSON serialization/deserialization support for all session types

Why This Change?

The previous implementation was tightly coupled to in-memory storage, making it impossible to share sessions across multiple ToolHive instances. This refactoring enables:

  • Horizontal scaling with shared session state
  • Session persistence across restarts
  • Future Redis/Valkey backend support without breaking changes
  • Better testability with pluggable storage backends

Testing

Added comprehensive unit tests covering:

  • LocalStorage implementation with all CRUD operations
  • ✅ Session serialization/deserialization for all session types
  • Manager with pluggable storage interface
  • ✅ All existing session types (ProxySession, SSESession, StreamableSession)
  • ✅ Error handling and edge cases
  • ✅ Context cancellation support
  • ✅ TTL and cleanup functionality

Backward Compatibility

Full backward compatibility maintained

  • Existing NewManager() function continues to work unchanged
  • All existing session types work without modification
  • No breaking changes to public APIs
  • Default behavior remains in-memory storage

Files Changed

  • pkg/transport/session/manager.go - Refactored to use Storage interface
  • pkg/transport/session/storage.go - New Storage interface and LocalStorage implementation
  • pkg/transport/session/serialization.go - JSON serialization support
  • pkg/transport/session/session.go - Enhanced session types with metadata support
  • pkg/transport/session/storage_test.go - Comprehensive test suite
  • pkg/transport/session/serialization_test.go - Serialization tests

Future Work

This foundation enables:

  • Redis/Valkey storage backend implementation
  • Distributed session management
  • Session replication and high availability
  • Custom storage backends for specific use cases

Checklist

  • All tests pass
  • Backward compatibility maintained
  • Documentation updated in code comments
  • No breaking changes to existing APIs
  • Comprehensive test coverage added
  • Error handling implemented
  • Context support added where appropriate

Refactors the session management system to use a pluggable storage
interface, enabling future support for distributed storage backends
like Redis/Valkey while maintaining backward compatibility.

What Changed

- Introduced a Storage interface that abstracts session persistence
- Refactored Manager to use the Storage interface instead of directly
  using sync.Map
- Created LocalStorage implementation that maintains the existing
  in-memory behavior
- Added JSON serialization support for sessions to enable future
  network storage
- Extended Session interface with Type() and metadata methods that
  were already implemented in concrete types

Why

The previous implementation was tightly coupled to in-memory storage,
making it impossible to share sessions across multiple ToolHive instances.
This refactoring enables:
- Horizontal scaling with shared session state
- Session persistence across restarts
- Future Redis/Valkey backend support without breaking changes

Testing

Added comprehensive unit tests covering:
- LocalStorage implementation
- Session serialization/deserialization
- Manager with pluggable storage
- All existing session types (ProxySession, SSESession, StreamableSession)

All tests pass and the implementation maintains full backward compatibility.

Signed-off-by: Juan Antonio Osorio <[email protected]>
Copy link

codecov bot commented Sep 21, 2025

Codecov Report

❌ Patch coverage is 89.56044% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.85%. Comparing base (26aee8c) to head (a0ba465).

Files with missing lines Patch % Lines
pkg/transport/session/serialization.go 88.00% 6 Missing and 3 partials ⚠️
pkg/transport/session/manager.go 87.50% 4 Missing and 1 partial ⚠️
pkg/transport/session/storage_local.go 92.53% 4 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1989      +/-   ##
==========================================
+ Coverage   47.38%   47.85%   +0.46%     
==========================================
  Files         232      234       +2     
  Lines       28644    28784     +140     
==========================================
+ Hits        13574    13775     +201     
+ Misses      14046    13986      -60     
+ Partials     1024     1023       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JAORMX JAORMX requested a review from Copilot September 23, 2025 15:00
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the session management system to use a pluggable storage interface, enabling future support for distributed storage backends like Redis/Valkey while maintaining backward compatibility with the existing in-memory behavior.

Key changes:

  • Introduced a Storage interface that abstracts session persistence operations
  • Refactored Manager to use the Storage interface instead of directly using sync.Map
  • Added JSON serialization support for sessions to enable future network storage
  • Enhanced session interfaces with metadata and data management methods

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pkg/transport/session/storage.go New Storage interface defining core operations for session persistence
pkg/transport/session/storage_local.go LocalStorage implementation using sync.Map for backward compatibility
pkg/transport/session/serialization.go JSON serialization functions for future distributed storage support
pkg/transport/session/manager.go Refactored Manager to use Storage interface instead of direct sync.Map
pkg/transport/session/storage_test.go Comprehensive test suite for storage implementations and serialization
pkg/transport/session/serialization_test.go Test suite for JSON serialization/deserialization functionality

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

return nil
}

// Close is a no-op for local storage as there are no resources to clean up.
Copy link
Preview

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is incorrect - the Close method actually clears all sessions, which is not a no-op. Update the comment to reflect the actual behavior.

Suggested change
// Close is a no-op for local storage as there are no resources to clean up.
// Close clears all sessions from local storage.

Copilot uses AI. Check for mistakes.

Comment on lines +100 to +103
s.sessions.Range(func(key, _ any) bool {
s.sessions.Delete(key)
return true
})
Copy link
Preview

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifying the sync.Map while iterating over it with Range can lead to race conditions. Consider collecting keys first, then deleting them in a separate loop, similar to the pattern used in DeleteExpired.

Suggested change
s.sessions.Range(func(key, _ any) bool {
s.sessions.Delete(key)
return true
})
var toDelete []any
s.sessions.Range(func(key, _ any) bool {
toDelete = append(toDelete, key)
return true
})
for _, key := range toDelete {
s.sessions.Delete(key)
}

Copilot uses AI. Check for mistakes.

sseSession := NewSSESession(sd.ID)
// Update timestamps to match stored values
sseSession.created = sd.CreatedAt
sseSession.updated = sd.UpdatedAt
Copy link
Preview

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct field assignment bypasses encapsulation and could break if the internal structure changes. Consider adding setter methods or using reflection-based copying instead of direct field access.

Suggested change
sseSession.updated = sd.UpdatedAt
sseSession.SetUpdatedAt(sd.UpdatedAt)

Copilot uses AI. Check for mistakes.

sseSession.created = sd.CreatedAt
sseSession.updated = sd.UpdatedAt
// Restore metadata
sseSession.metadata = sd.Metadata
Copy link
Preview

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct field assignment bypasses encapsulation and could break if the internal structure changes. Consider adding setter methods or using reflection-based copying instead of direct field access.

Copilot uses AI. Check for mistakes.

Comment on lines +85 to +87
streamSession.created = sd.CreatedAt
streamSession.updated = sd.UpdatedAt
streamSession.sessType = SessionTypeStreamable
Copy link
Preview

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct field assignment bypasses encapsulation and could break if the internal structure changes. Consider adding setter methods or using reflection-based copying instead of direct field access.

Copilot uses AI. Check for mistakes.

Comment on lines +89 to +92
streamSession.metadata = sd.Metadata
if streamSession.metadata == nil {
streamSession.metadata = make(map[string]string)
}
Copy link
Preview

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct field assignment bypasses encapsulation and could break if the internal structure changes. Consider adding setter methods or using reflection-based copying instead of direct field access.

Suggested change
streamSession.metadata = sd.Metadata
if streamSession.metadata == nil {
streamSession.metadata = make(map[string]string)
}
streamSession.SetMetadata(sd.Metadata)
// SetMetadata handles nil initialization

Copilot uses AI. Check for mistakes.

Comment on lines +101 to +102
proxySession.created = sd.CreatedAt
proxySession.updated = sd.UpdatedAt
Copy link
Preview

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct field assignment bypasses encapsulation and could break if the internal structure changes. Consider adding setter methods or using reflection-based copying instead of direct field access.

Copilot uses AI. Check for mistakes.

proxySession.created = sd.CreatedAt
proxySession.updated = sd.UpdatedAt
// Restore metadata
proxySession.metadata = sd.Metadata
Copy link
Preview

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct field assignment bypasses encapsulation and could break if the internal structure changes. Consider adding setter methods or using reflection-based copying instead of direct field access.

Copilot uses AI. Check for mistakes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant