feat: enhance API and session management with Nacos and Redis integration

- Add Nacos registry for service registration and deregistration.
- Implement Redis registry for session management with heartbeat and session claiming.
- Improve completion service with session handling and request validation.
- Enhance WebSocket handling for completion requests with JSON-RPC support.
- Add tests for new registry implementations and completion manager functionalities.
- Refactor existing code for better readability and maintainability.
This commit is contained in:
2026-02-15 17:46:34 +08:00
parent 57afb90bc0
commit 3284ce07c7
22 changed files with 1863 additions and 87 deletions

View File

@@ -13,35 +13,40 @@ import (
var ErrUnsupportedLanguage = errors.New("unsupported language")
var ErrTooManySessions = errors.New("too many active lsp sessions")
// RuntimeClient 是带生命周期管理能力的补全客户端。
type RuntimeClient interface {
Client
Close() error
}
// SessionRegistry 抽象跨实例会话归属协调能力(如 Redis
type SessionRegistry interface {
ClaimSession(ctx context.Context, language, sessionID string) (ownerID string, ownerEndpoint string, err error)
ReleaseSession(ctx context.Context, language, sessionID string) error
Close() error
}
// LanguageServerSpec 描述某种语言对应的 LSP 启动参数。
type LanguageServerSpec struct {
Language string
LanguageID string
Command string
Args []string
Language string // 语言名(如 go/typescript用于路由匹配。
LanguageID string // 传给 LSP 的 languageId。
Command string // LSP 可执行命令。
Args []string // LSP 启动参数。
}
type ClientFactory func(ctx context.Context, spec LanguageServerSpec, workspaceDir string) (RuntimeClient, error)
// ManagerConfig 控制会话池容量、TTL 与实例信息。
type ManagerConfig struct {
WorkspaceDir string
MaxSessions int
SessionTTL time.Duration
CleanupInterval time.Duration
InstanceID string
Registry SessionRegistry
WorkspaceDir string // LSP 进程工作区目录。
MaxSessions int // 本实例会话上限。
SessionTTL time.Duration // 会话空闲超时。
CleanupInterval time.Duration // 会话清理周期。
InstanceID string // 当前实例 ID。
Registry SessionRegistry // 可选分布式会话注册中心。
}
// Manager 按 language/session 复用 LSP 会话,并负责清理与淘汰。
type Manager struct {
mu sync.Mutex
@@ -63,6 +68,7 @@ type managedSession struct {
createdAt time.Time
}
// ErrSessionOwnedByOtherInstance 表示会话已被其他实例持有。
type ErrSessionOwnedByOtherInstance struct {
OwnerID string
OwnerEndpoint string
@@ -75,6 +81,7 @@ func (e *ErrSessionOwnedByOtherInstance) Error() string {
return fmt.Sprintf("session owned by another instance: %s", e.OwnerID)
}
// NewManager 构建会话管理器并启动后台清理协程。
func NewManager(config ManagerConfig, specs []LanguageServerSpec, factory ClientFactory) *Manager {
if config.MaxSessions <= 0 {
config.MaxSessions = 256
@@ -109,6 +116,7 @@ func NewManager(config ManagerConfig, specs []LanguageServerSpec, factory Client
return m
}
// Complete 处理补全请求,包含语言匹配、会话归属、会话复用/创建。
func (m *Manager) Complete(ctx context.Context, req Request) (Response, error) {
language := normalizeLanguage(req.Language)
if language == "" {
@@ -124,6 +132,7 @@ func (m *Manager) Complete(ctx context.Context, req Request) (Response, error) {
sessionID := normalizeSessionID(req.SessionID)
if m.config.Registry != nil {
// 分布式模式下先声明会话归属,避免多实例并发写同一会话。
ownerID, ownerEndpoint, err := m.config.Registry.ClaimSession(ctx, language, sessionID)
if err != nil {
return Response{}, err
@@ -154,6 +163,7 @@ func (m *Manager) Complete(ctx context.Context, req Request) (Response, error) {
return resp, nil
}
// ActiveSessions 统计当前实例内各语言活跃会话数。
func (m *Manager) ActiveSessions() map[string]int {
m.mu.Lock()
defer m.mu.Unlock()
@@ -165,6 +175,7 @@ func (m *Manager) ActiveSessions() map[string]int {
return out
}
// Close 停止后台任务并释放所有会话与注册中心资源。
func (m *Manager) Close() error {
m.stoppedOnce.Do(func() {
close(m.stopCh)
@@ -186,6 +197,7 @@ func (m *Manager) Close() error {
return nil
}
// cleanupLoop 周期清理闲置会话。
func (m *Manager) cleanupLoop() {
ticker := time.NewTicker(m.config.CleanupInterval)
defer ticker.Stop()
@@ -200,6 +212,7 @@ func (m *Manager) cleanupLoop() {
}
}
// cleanupIdleSessions 关闭超过 TTL 未使用的会话。
func (m *Manager) cleanupIdleSessions() {
cutoff := time.Now().Add(-m.config.SessionTTL)
@@ -218,6 +231,7 @@ func (m *Manager) cleanupIdleSessions() {
}
}
// getOrCreateSession 返回已有会话,或按需新建一个会话。
func (m *Manager) getOrCreateSession(
ctx context.Context,
sessionKey string,
@@ -258,6 +272,7 @@ func (m *Manager) getOrCreateSession(
m.mu.Lock()
defer m.mu.Unlock()
if existing, ok := m.sessions[sessionKey]; ok {
// 并发竞争下可能已经被其他协程创建,直接复用并关闭新 client。
_ = client.Close()
existing.lastUsed = now
return existing, nil
@@ -266,6 +281,7 @@ func (m *Manager) getOrCreateSession(
return newSession, nil
}
// evictLeastRecentlyUsedLocked 在达到上限时淘汰最久未使用会话。
func (m *Manager) evictLeastRecentlyUsedLocked() bool {
if len(m.sessions) == 0 {
return false
@@ -301,6 +317,7 @@ func buildSessionKey(language, sessionID string) string {
return language + ":" + normalizeSessionID(sessionID)
}
// normalizeSessionID 将空 session 归一为 default便于复用同一会话键。
func normalizeSessionID(sessionID string) string {
sid := strings.TrimSpace(sessionID)
if sid == "" {
@@ -309,6 +326,7 @@ func normalizeSessionID(sessionID string) string {
return sid
}
// normalizeLanguage 统一语言名大小写和空白。
func normalizeLanguage(language string) string {
return strings.ToLower(strings.TrimSpace(language))
}