From ae37055331e7738470bb5ce253246442b4d82ca8 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Wed, 27 Aug 2025 20:54:26 +0200 Subject: [PATCH] Add onStatusChange callback to instance management for status updates --- pkg/instance/instance.go | 7 +++++-- pkg/instance/instance_test.go | 30 ++++++++++++++++++++++++------ pkg/instance/status.go | 8 ++++++-- pkg/instance/timeout_test.go | 30 ++++++++++++++++++++++++------ pkg/manager/manager.go | 21 ++++++++++++++++++--- pkg/manager/operations.go | 6 +++++- 6 files changed, 82 insertions(+), 20 deletions(-) diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go index 862c323..3780f84 100644 --- a/pkg/instance/instance.go +++ b/pkg/instance/instance.go @@ -82,7 +82,8 @@ type Process struct { globalSettings *config.InstancesConfig // Status - Status InstanceStatus `json:"status"` + Status InstanceStatus `json:"status"` + onStatusChange func(oldStatus, newStatus InstanceStatus) // Creation time Created int64 `json:"created,omitempty"` // Unix timestamp when the instance was created @@ -193,7 +194,7 @@ func applyDefaultOptions(options *CreateInstanceOptions, globalSettings *config. } // NewInstance creates a new instance with the given name, log path, and options -func NewInstance(name string, globalSettings *config.InstancesConfig, options *CreateInstanceOptions) *Process { +func NewInstance(name string, globalSettings *config.InstancesConfig, options *CreateInstanceOptions, onStatusChange func(oldStatus, newStatus InstanceStatus)) *Process { // Validate and copy options optionsCopy := validateAndCopyOptions(name, options) // Apply defaults @@ -208,6 +209,8 @@ func NewInstance(name string, globalSettings *config.InstancesConfig, options *C logger: logger, timeProvider: realTimeProvider{}, Created: time.Now().Unix(), + Status: Stopped, + onStatusChange: onStatusChange, } } diff --git a/pkg/instance/instance_test.go b/pkg/instance/instance_test.go index 5582f04..4f30ab6 100644 --- a/pkg/instance/instance_test.go +++ b/pkg/instance/instance_test.go @@ -24,7 +24,10 @@ func TestNewInstance(t *testing.T) { }, } - instance := instance.NewInstance("test-instance", globalSettings, options) + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + + instance := instance.NewInstance("test-instance", globalSettings, options, mockOnStatusChange) if instance.Name != "test-instance" { t.Errorf("Expected name 'test-instance', got %q", instance.Name) @@ -76,7 +79,10 @@ func TestNewInstance_WithRestartOptions(t *testing.T) { }, } - instance := instance.NewInstance("test-instance", globalSettings, options) + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + + instance := instance.NewInstance("test-instance", globalSettings, options, mockOnStatusChange) opts := instance.GetOptions() // Check that explicit values override defaults @@ -106,7 +112,10 @@ func TestSetOptions(t *testing.T) { }, } - inst := instance.NewInstance("test-instance", globalSettings, initialOptions) + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + + inst := instance.NewInstance("test-instance", globalSettings, initialOptions, mockOnStatusChange) // Update options newOptions := &instance.CreateInstanceOptions{ @@ -144,7 +153,10 @@ func TestGetProxy(t *testing.T) { }, } - inst := instance.NewInstance("test-instance", globalSettings, options) + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + + inst := instance.NewInstance("test-instance", globalSettings, options, mockOnStatusChange) // Get proxy for the first time proxy1, err := inst.GetProxy() @@ -180,7 +192,10 @@ func TestMarshalJSON(t *testing.T) { }, } - instance := instance.NewInstance("test-instance", globalSettings, options) + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + + instance := instance.NewInstance("test-instance", globalSettings, options, mockOnStatusChange) data, err := json.Marshal(instance) if err != nil { @@ -303,7 +318,10 @@ func TestCreateInstanceOptionsValidation(t *testing.T) { }, } - instance := instance.NewInstance("test", globalSettings, options) + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + + instance := instance.NewInstance("test", globalSettings, options, mockOnStatusChange) opts := instance.GetOptions() if opts.MaxRestarts == nil { diff --git a/pkg/instance/status.go b/pkg/instance/status.go index c7e987a..3738502 100644 --- a/pkg/instance/status.go +++ b/pkg/instance/status.go @@ -28,9 +28,13 @@ var statusToName = map[InstanceStatus]string{ func (p *Process) SetStatus(status InstanceStatus) { p.mu.Lock() - defer p.mu.Unlock() - + oldStatus := p.Status p.Status = status + p.mu.Unlock() + + if p.onStatusChange != nil { + p.onStatusChange(oldStatus, status) + } } func (p *Process) GetStatus() InstanceStatus { diff --git a/pkg/instance/timeout_test.go b/pkg/instance/timeout_test.go index e6ac531..05abd04 100644 --- a/pkg/instance/timeout_test.go +++ b/pkg/instance/timeout_test.go @@ -42,7 +42,10 @@ func TestUpdateLastRequestTime(t *testing.T) { }, } - inst := instance.NewInstance("test-instance", globalSettings, options) + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + + inst := instance.NewInstance("test-instance", globalSettings, options, mockOnStatusChange) // Test that UpdateLastRequestTime doesn't panic inst.UpdateLastRequestTime() @@ -61,7 +64,10 @@ func TestShouldTimeout_NotRunning(t *testing.T) { }, } - inst := instance.NewInstance("test-instance", globalSettings, options) + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + + inst := instance.NewInstance("test-instance", globalSettings, options, mockOnStatusChange) // Instance is not running, should not timeout regardless of configuration if inst.ShouldTimeout() { @@ -85,6 +91,9 @@ func TestShouldTimeout_NoTimeoutConfigured(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + options := &instance.CreateInstanceOptions{ IdleTimeout: tt.idleTimeout, LlamaServerOptions: llamacpp.LlamaServerOptions{ @@ -92,7 +101,7 @@ func TestShouldTimeout_NoTimeoutConfigured(t *testing.T) { }, } - inst := instance.NewInstance("test-instance", globalSettings, options) + inst := instance.NewInstance("test-instance", globalSettings, options, mockOnStatusChange) // Simulate running state inst.SetStatus(instance.Running) @@ -116,7 +125,10 @@ func TestShouldTimeout_WithinTimeLimit(t *testing.T) { }, } - inst := instance.NewInstance("test-instance", globalSettings, options) + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + + inst := instance.NewInstance("test-instance", globalSettings, options, mockOnStatusChange) inst.SetStatus(instance.Running) // Update last request time to now @@ -141,7 +153,10 @@ func TestShouldTimeout_ExceedsTimeLimit(t *testing.T) { }, } - inst := instance.NewInstance("test-instance", globalSettings, options) + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + + inst := instance.NewInstance("test-instance", globalSettings, options, mockOnStatusChange) inst.SetStatus(instance.Running) // Use MockTimeProvider to simulate old last request time @@ -184,7 +199,10 @@ func TestTimeoutConfiguration_Validation(t *testing.T) { }, } - inst := instance.NewInstance("test-instance", globalSettings, options) + // Mock onStatusChange function + mockOnStatusChange := func(oldStatus, newStatus instance.InstanceStatus) {} + + inst := instance.NewInstance("test-instance", globalSettings, options, mockOnStatusChange) opts := inst.GetOptions() if opts.IdleTimeout == nil || *opts.IdleTimeout != tt.expectedTimeout { diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index 4114eb1..90ef27b 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -30,7 +30,7 @@ type InstanceManager interface { type instanceManager struct { mu sync.RWMutex instances map[string]*instance.Process - runningInstances map[*instance.Process]struct{} + runningInstances map[string]struct{} ports map[int]bool instancesConfig config.InstancesConfig @@ -48,7 +48,7 @@ func NewInstanceManager(instancesConfig config.InstancesConfig) InstanceManager } im := &instanceManager{ instances: make(map[string]*instance.Process), - runningInstances: make(map[*instance.Process]struct{}), + runningInstances: make(map[string]struct{}), ports: make(map[int]bool), instancesConfig: instancesConfig, @@ -229,8 +229,12 @@ func (im *instanceManager) loadInstance(name, path string) error { return fmt.Errorf("instance name mismatch: file=%s, instance.Name=%s", name, persistedInstance.Name) } + statusCallback := func(oldStatus, newStatus instance.InstanceStatus) { + im.onStatusChange(persistedInstance.Name, oldStatus, newStatus) + } + // Create new inst using NewInstance (handles validation, defaults, setup) - inst := instance.NewInstance(name, &im.instancesConfig, persistedInstance.GetOptions()) + inst := instance.NewInstance(name, &im.instancesConfig, persistedInstance.GetOptions(), statusCallback) // Restore persisted fields that NewInstance doesn't set inst.Created = persistedInstance.Created @@ -272,3 +276,14 @@ func (im *instanceManager) autoStartInstances() { } } } + +func (im *instanceManager) onStatusChange(name string, oldStatus, newStatus instance.InstanceStatus) { + im.mu.Lock() + defer im.mu.Unlock() + + if newStatus == instance.Running { + im.runningInstances[name] = struct{}{} + } else { + delete(im.runningInstances, name) + } +} diff --git a/pkg/manager/operations.go b/pkg/manager/operations.go index d7d06b9..73b3c70 100644 --- a/pkg/manager/operations.go +++ b/pkg/manager/operations.go @@ -65,7 +65,11 @@ func (im *instanceManager) CreateInstance(name string, options *instance.CreateI im.ports[options.Port] = true } - inst := instance.NewInstance(name, &im.instancesConfig, options) + statusCallback := func(oldStatus, newStatus instance.InstanceStatus) { + im.onStatusChange(name, oldStatus, newStatus) + } + + inst := instance.NewInstance(name, &im.instancesConfig, options, statusCallback) im.instances[inst.Name] = inst im.ports[options.Port] = true