yuan-lab-llm--clawmanager
cfd9c46ca8
Address three HIGH-severity issues from the memory-leak and bloat analysis (issue #56). 1. Graceful shutdown (main.go) - Replace gin's r.Run() with an explicit http.Server so the process can intercept SIGINT / SIGTERM. - On signal: drain active HTTP requests (10 s timeout), then stop SyncService, WebSocket Hub, and InstanceAccessService cleanup goroutine in order. - Ensures database connections, K8s watchers, and background loops are released cleanly on deploy or restart. 2. WebSocket Hub init race (websocket_service.go) - GetHub() used a bare nil-check with no synchronisation; two goroutines could each create a Hub and start a Run() loop. - Replaced with sync.Once to guarantee exactly one Hub instance. - Added a stop channel to Hub.Run() so the hub can be shut down gracefully, closing all connected clients. 3. InstanceAccessService goroutine leak (instance_access_service.go) - cleanupExpiredTokens() looped on ticker.C with no exit path, leaking the goroutine for the lifetime of the process. - Added a stopChan; cleanupExpiredTokens now selects on both the ticker and the stop signal. - Exposed Stop() on the service; InstanceHandler.Shutdown() calls it during graceful shutdown. Tests: - TestGetHubReturnsSameInstance: singleton guarantee - TestGetHubConcurrentAccess: 50-goroutine race test - TestHubStopClosesClients: verifies client cleanup on Stop() - TestInstanceAccessServiceStopTerminatesCleanup: Stop() is safe and the service remains functional for token ops afterward All existing tests continue to pass; full project build and regression verified. Ref: #56
92 行
2.7 KiB
Go
92 行
2.7 KiB
Go
package services
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestInstanceAccessServiceValidatesTokenAcrossServiceInstances(t *testing.T) {
|
|
t.Setenv("INSTANCE_ACCESS_TOKEN_SECRET", "cluster-shared-secret")
|
|
|
|
issuer := NewInstanceAccessService()
|
|
validator := NewInstanceAccessService()
|
|
|
|
token, err := issuer.GenerateToken(7, 42, "openclaw", "/api/v1/instances/42/proxy/", 3001, 5*time.Minute)
|
|
if err != nil {
|
|
t.Fatalf("GenerateToken() error = %v", err)
|
|
}
|
|
|
|
validated, err := validator.ValidateToken(token.Token)
|
|
if err != nil {
|
|
t.Fatalf("ValidateToken() error = %v", err)
|
|
}
|
|
|
|
if validated.InstanceID != 42 {
|
|
t.Fatalf("validated.InstanceID = %d, want 42", validated.InstanceID)
|
|
}
|
|
if validated.UserID != 7 {
|
|
t.Fatalf("validated.UserID = %d, want 7", validated.UserID)
|
|
}
|
|
if validated.InstanceType != "openclaw" {
|
|
t.Fatalf("validated.InstanceType = %q, want openclaw", validated.InstanceType)
|
|
}
|
|
}
|
|
|
|
func TestInstanceAccessServiceRejectsExpiredSignedToken(t *testing.T) {
|
|
t.Setenv("INSTANCE_ACCESS_TOKEN_SECRET", "cluster-shared-secret")
|
|
|
|
service := NewInstanceAccessService()
|
|
token, err := service.GenerateToken(7, 42, "openclaw", "/api/v1/instances/42/proxy/", 3001, -time.Second)
|
|
if err != nil {
|
|
t.Fatalf("GenerateToken() error = %v", err)
|
|
}
|
|
|
|
if _, err := service.ValidateToken(token.Token); err == nil || err.Error() != "token expired" {
|
|
t.Fatalf("ValidateToken() error = %v, want token expired", err)
|
|
}
|
|
}
|
|
|
|
func TestInstanceAccessServiceFallsBackToLegacyTokens(t *testing.T) {
|
|
t.Setenv("INSTANCE_ACCESS_TOKEN_SECRET", "cluster-shared-secret")
|
|
|
|
service := NewInstanceAccessService()
|
|
service.tokens["legacy-token"] = &AccessToken{
|
|
Token: "legacy-token",
|
|
InstanceID: 11,
|
|
UserID: 3,
|
|
InstanceType: "ubuntu",
|
|
TargetPort: 3001,
|
|
AccessURL: "/api/v1/instances/11/proxy/",
|
|
ExpiresAt: time.Now().Add(time.Minute),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
validated, err := service.ValidateToken("legacy-token")
|
|
if err != nil {
|
|
t.Fatalf("ValidateToken() error = %v", err)
|
|
}
|
|
|
|
if validated.InstanceID != 11 {
|
|
t.Fatalf("validated.InstanceID = %d, want 11", validated.InstanceID)
|
|
}
|
|
}
|
|
|
|
func TestInstanceAccessServiceStopTerminatesCleanup(t *testing.T) {
|
|
t.Setenv("INSTANCE_ACCESS_TOKEN_SECRET", "cluster-shared-secret")
|
|
|
|
service := NewInstanceAccessService()
|
|
|
|
// Stop should not panic and should be idempotent-safe (only called once).
|
|
service.Stop()
|
|
|
|
// After Stop, the service should still be usable for token operations
|
|
// (only the background cleanup goroutine is stopped).
|
|
token, err := service.GenerateToken(1, 1, "openclaw", "/proxy", 3001, time.Minute)
|
|
if err != nil {
|
|
t.Fatalf("GenerateToken after Stop() error = %v", err)
|
|
}
|
|
if _, err := service.ValidateToken(token.Token); err != nil {
|
|
t.Fatalf("ValidateToken after Stop() error = %v", err)
|
|
}
|
|
}
|