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:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user