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))
}

View File

@@ -31,6 +31,7 @@ func (f *fakeRuntimeClient) Close() error {
return nil
}
// 同 language+session 的请求应复用同一个底层 client。
func TestManagerCompleteSelectsLanguageAndSession(t *testing.T) {
createCount := 0
factory := func(_ context.Context, spec LanguageServerSpec, _ string) (RuntimeClient, error) {
@@ -68,6 +69,7 @@ func TestManagerCompleteSelectsLanguageAndSession(t *testing.T) {
}
}
// 未配置的语言应返回 ErrUnsupportedLanguage。
func TestManagerCompleteUnsupportedLanguage(t *testing.T) {
m := NewManager(ManagerConfig{}, []LanguageServerSpec{
{Language: "go", LanguageID: "go", Command: "gopls"},
@@ -88,6 +90,7 @@ func TestManagerCompleteUnsupportedLanguage(t *testing.T) {
}
}
// 超过空闲 TTL 的会话应被后台清理并关闭 client。
func TestManagerCleanupIdleSession(t *testing.T) {
client := &fakeRuntimeClient{}
m := NewManager(ManagerConfig{

View File

@@ -9,6 +9,7 @@ import (
var ErrInvalidRequest = errors.New("invalid completion request")
// Request 是统一的补全请求模型。
type Request struct {
Language string `json:"language,omitempty"`
SessionID string `json:"sessionId,omitempty"`
@@ -18,6 +19,7 @@ type Request struct {
Character int `json:"character"`
}
// Item 对应一个补全候选项。
type Item struct {
Label string `json:"label"`
Kind int `json:"kind,omitempty"`
@@ -28,11 +30,13 @@ type Item struct {
FilterText string `json:"filterText,omitempty"`
}
// Response 是补全结果集合。
type Response struct {
Items []Item `json:"items"`
IsIncomplete bool `json:"isIncomplete"`
}
// Client 抽象 LSP 客户端所需的最小能力。
type Client interface {
DidOpen(ctx context.Context, uri, text string, version int) error
DidChange(ctx context.Context, uri, text string, version int) error
@@ -43,6 +47,7 @@ type documentState struct {
version int
}
// Service 负责文档生命周期同步didOpen/didChange与补全调用。
type Service struct {
client Client
@@ -57,6 +62,7 @@ func NewService(client Client) *Service {
}
}
// Complete 根据 URI 的历史状态决定发送 didOpen 或 didChange再请求补全。
func (s *Service) Complete(ctx context.Context, req Request) (Response, error) {
if err := validateRequest(req); err != nil {
return Response{}, err
@@ -86,6 +92,7 @@ func (s *Service) Complete(ctx context.Context, req Request) (Response, error) {
return resp, nil
}
// validateRequest 做基础参数校验,避免向 LSP 发送非法请求。
func validateRequest(req Request) error {
if req.URI == "" {
return ErrInvalidRequest

View File

@@ -52,6 +52,7 @@ func (f *fakeLSPClient) Completion(_ context.Context, uri string, line, characte
return f.completionResp, nil
}
// 首次请求应发送 didOpen并继续请求 completion。
func TestServiceCompleteFirstRequestSendsDidOpen(t *testing.T) {
fake := &fakeLSPClient{
completionResp: Response{Items: []Item{{Label: "Println"}}, IsIncomplete: true},
@@ -84,6 +85,7 @@ func TestServiceCompleteFirstRequestSendsDidOpen(t *testing.T) {
}
}
// 同一文档第二次请求应发送 didChange且版本号递增。
func TestServiceCompleteSecondRequestUsesDidChange(t *testing.T) {
fake := &fakeLSPClient{}
svc := NewService(fake)
@@ -119,6 +121,7 @@ func TestServiceCompleteSecondRequestUsesDidChange(t *testing.T) {
}
}
// 参数不合法时应直接返回 ErrInvalidRequest。
func TestServiceCompleteValidatesRequest(t *testing.T) {
svc := NewService(&fakeLSPClient{})
@@ -128,6 +131,7 @@ func TestServiceCompleteValidatesRequest(t *testing.T) {
}
}
// 底层 client 出错应向上透传错误。
func TestServiceCompleteReturnsClientError(t *testing.T) {
fake := &fakeLSPClient{openErr: errors.New("open failed")}
svc := NewService(fake)