diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go index f690ca1..862c323 100644 --- a/pkg/instance/instance.go +++ b/pkg/instance/instance.go @@ -82,7 +82,7 @@ type Process struct { globalSettings *config.InstancesConfig // Status - Running bool `json:"running"` + Status InstanceStatus `json:"status"` // Creation time Created int64 `json:"created,omitempty"` // Unix timestamp when the instance was created @@ -287,12 +287,12 @@ func (i *Process) MarshalJSON() ([]byte, error) { temp := struct { Name string `json:"name"` Options *CreateInstanceOptions `json:"options,omitempty"` - Running bool `json:"running"` + Status InstanceStatus `json:"status"` Created int64 `json:"created,omitempty"` }{ Name: i.Name, Options: i.options, - Running: i.Running, + Status: i.Status, Created: i.Created, } @@ -305,7 +305,7 @@ func (i *Process) UnmarshalJSON(data []byte) error { temp := struct { Name string `json:"name"` Options *CreateInstanceOptions `json:"options,omitempty"` - Running bool `json:"running"` + Status InstanceStatus `json:"status"` Created int64 `json:"created,omitempty"` }{} @@ -315,7 +315,7 @@ func (i *Process) UnmarshalJSON(data []byte) error { // Set the fields i.Name = temp.Name - i.Running = temp.Running + i.Status = temp.Status i.Created = temp.Created // Handle options with validation but no defaults diff --git a/pkg/instance/instance_test.go b/pkg/instance/instance_test.go index 3ed6c08..5582f04 100644 --- a/pkg/instance/instance_test.go +++ b/pkg/instance/instance_test.go @@ -29,7 +29,7 @@ func TestNewInstance(t *testing.T) { if instance.Name != "test-instance" { t.Errorf("Expected name 'test-instance', got %q", instance.Name) } - if instance.Running { + if instance.IsRunning() { t.Error("New instance should not be running") } @@ -188,7 +188,7 @@ func TestMarshalJSON(t *testing.T) { } // Check that JSON contains expected fields - var result map[string]interface{} + var result map[string]any err = json.Unmarshal(data, &result) if err != nil { t.Fatalf("JSON unmarshal failed: %v", err) @@ -197,8 +197,8 @@ func TestMarshalJSON(t *testing.T) { if result["name"] != "test-instance" { t.Errorf("Expected name 'test-instance', got %v", result["name"]) } - if result["running"] != false { - t.Errorf("Expected running false, got %v", result["running"]) + if result["status"] != "stopped" { + t.Errorf("Expected status 'stopped', got %v", result["status"]) } // Check that options are included @@ -218,7 +218,7 @@ func TestMarshalJSON(t *testing.T) { func TestUnmarshalJSON(t *testing.T) { jsonData := `{ "name": "test-instance", - "running": true, + "status": "running", "options": { "model": "/path/to/model.gguf", "port": 8080, @@ -236,8 +236,8 @@ func TestUnmarshalJSON(t *testing.T) { if inst.Name != "test-instance" { t.Errorf("Expected name 'test-instance', got %q", inst.Name) } - if !inst.Running { - t.Error("Expected running to be true") + if !inst.IsRunning() { + t.Error("Expected status to be running") } opts := inst.GetOptions() diff --git a/pkg/instance/lifecycle.go b/pkg/instance/lifecycle.go index eb2fc83..53a4091 100644 --- a/pkg/instance/lifecycle.go +++ b/pkg/instance/lifecycle.go @@ -11,18 +11,12 @@ import ( "time" ) -func (i *Process) IsRunning() bool { - i.mu.RLock() - defer i.mu.RUnlock() - return i.Running -} - // Start starts the llama server instance and returns an error if it fails. func (i *Process) Start() error { i.mu.Lock() defer i.mu.Unlock() - if i.Running { + if i.IsRunning() { return fmt.Errorf("instance %s is already running", i.Name) } @@ -71,7 +65,7 @@ func (i *Process) Start() error { return fmt.Errorf("failed to start instance %s: %w", i.Name, err) } - i.Running = true + i.SetStatus(Running) // Create channel for monitor completion signaling i.monitorDone = make(chan struct{}) @@ -88,7 +82,7 @@ func (i *Process) Start() error { func (i *Process) Stop() error { i.mu.Lock() - if !i.Running { + if !i.IsRunning() { // Even if not running, cancel any pending restart if i.restartCancel != nil { i.restartCancel() @@ -105,8 +99,8 @@ func (i *Process) Stop() error { i.restartCancel = nil } - // Set running to false first to signal intentional stop - i.Running = false + // Set status to stopped first to signal intentional stop + i.SetStatus(Stopped) // Clean up the proxy i.proxy = nil @@ -151,7 +145,7 @@ func (i *Process) Stop() error { } func (i *Process) WaitForHealthy(timeout int) error { - if !i.Running { + if !i.IsRunning() { return fmt.Errorf("instance %s is not running", i.Name) } @@ -233,12 +227,12 @@ func (i *Process) monitorProcess() { i.mu.Lock() // Check if the instance was intentionally stopped - if !i.Running { + if !i.IsRunning() { i.mu.Unlock() return } - i.Running = false + i.SetStatus(Stopped) i.logger.Close() // Cancel any existing restart context since we're handling a new exit diff --git a/pkg/instance/status.go b/pkg/instance/status.go new file mode 100644 index 0000000..c7e987a --- /dev/null +++ b/pkg/instance/status.go @@ -0,0 +1,72 @@ +package instance + +import ( + "encoding/json" + "log" +) + +// Enum for instance status +type InstanceStatus int + +const ( + Stopped InstanceStatus = iota + Running + Failed +) + +var nameToStatus = map[string]InstanceStatus{ + "stopped": Stopped, + "running": Running, + "failed": Failed, +} + +var statusToName = map[InstanceStatus]string{ + Stopped: "stopped", + Running: "running", + Failed: "failed", +} + +func (p *Process) SetStatus(status InstanceStatus) { + p.mu.Lock() + defer p.mu.Unlock() + + p.Status = status +} + +func (p *Process) GetStatus() InstanceStatus { + p.mu.RLock() + defer p.mu.RUnlock() + return p.Status +} + +// IsRunning returns true if the status is Running +func (p *Process) IsRunning() bool { + p.mu.RLock() + defer p.mu.RUnlock() + return p.Status == Running +} + +func (s InstanceStatus) MarshalJSON() ([]byte, error) { + name, ok := statusToName[s] + if !ok { + name = "stopped" // Default to "stopped" for unknown status + } + return json.Marshal(name) +} + +// UnmarshalJSON implements json.Unmarshaler +func (s *InstanceStatus) UnmarshalJSON(data []byte) error { + var str string + if err := json.Unmarshal(data, &str); err != nil { + return err + } + + status, ok := nameToStatus[str] + if !ok { + log.Printf("Unknown instance status: %s", str) + status = Stopped // Default to Stopped on unknown status + } + + *s = status + return nil +} diff --git a/pkg/instance/timeout.go b/pkg/instance/timeout.go index 19344e6..94cdc16 100644 --- a/pkg/instance/timeout.go +++ b/pkg/instance/timeout.go @@ -13,7 +13,7 @@ func (i *Process) ShouldTimeout() bool { i.mu.RLock() defer i.mu.RUnlock() - if !i.Running || i.options.IdleTimeout == nil || *i.options.IdleTimeout <= 0 { + if !i.IsRunning() || i.options.IdleTimeout == nil || *i.options.IdleTimeout <= 0 { return false } diff --git a/pkg/instance/timeout_test.go b/pkg/instance/timeout_test.go index 5cd67f2..e6ac531 100644 --- a/pkg/instance/timeout_test.go +++ b/pkg/instance/timeout_test.go @@ -94,7 +94,7 @@ func TestShouldTimeout_NoTimeoutConfigured(t *testing.T) { inst := instance.NewInstance("test-instance", globalSettings, options) // Simulate running state - inst.Running = true + inst.SetStatus(instance.Running) if inst.ShouldTimeout() { t.Errorf("Instance with %s should not timeout", tt.name) @@ -117,7 +117,7 @@ func TestShouldTimeout_WithinTimeLimit(t *testing.T) { } inst := instance.NewInstance("test-instance", globalSettings, options) - inst.Running = true + inst.SetStatus(instance.Running) // Update last request time to now inst.UpdateLastRequestTime() @@ -142,7 +142,7 @@ func TestShouldTimeout_ExceedsTimeLimit(t *testing.T) { } inst := instance.NewInstance("test-instance", globalSettings, options) - inst.Running = true + inst.SetStatus(instance.Running) // Use MockTimeProvider to simulate old last request time mockTime := NewMockTimeProvider(time.Now()) diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index 31c0b01..4114eb1 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -150,7 +150,7 @@ func (im *instanceManager) Shutdown() { wg.Add(len(im.instances)) for name, inst := range im.instances { - if !inst.Running { + if !inst.IsRunning() { wg.Done() // If instance is not running, just mark it as done continue } @@ -234,7 +234,7 @@ func (im *instanceManager) loadInstance(name, path string) error { // Restore persisted fields that NewInstance doesn't set inst.Created = persistedInstance.Created - inst.Running = persistedInstance.Running + inst.SetStatus(persistedInstance.Status) // Check for port conflicts and add to maps if inst.GetOptions() != nil && inst.GetOptions().Port > 0 { @@ -254,7 +254,7 @@ func (im *instanceManager) autoStartInstances() { im.mu.RLock() var instancesToStart []*instance.Process for _, inst := range im.instances { - if inst.Running && // Was running when persisted + if inst.IsRunning() && // Was running when persisted inst.GetOptions() != nil && inst.GetOptions().AutoRestart != nil && *inst.GetOptions().AutoRestart { @@ -266,7 +266,7 @@ func (im *instanceManager) autoStartInstances() { for _, inst := range instancesToStart { log.Printf("Auto-starting instance %s", inst.Name) // Reset running state before starting (since Start() expects stopped instance) - inst.Running = false + inst.SetStatus(instance.Stopped) if err := inst.Start(); err != nil { log.Printf("Failed to auto-start instance %s: %v", inst.Name, err) } diff --git a/pkg/manager/manager_test.go b/pkg/manager/manager_test.go index 6c48366..df675dc 100644 --- a/pkg/manager/manager_test.go +++ b/pkg/manager/manager_test.go @@ -59,7 +59,7 @@ func TestCreateInstance_Success(t *testing.T) { if inst.Name != "test-instance" { t.Errorf("Expected instance name 'test-instance', got %q", inst.Name) } - if inst.Running { + if inst.GetStatus() != instance.Stopped { t.Error("New instance should not be running") } if inst.GetOptions().Port != 8080 { @@ -357,7 +357,7 @@ func TestTimeoutFunctionality(t *testing.T) { inst.SetTimeProvider(mockTime) // Set instance to running state so timeout logic can work - inst.Running = true + inst.SetStatus(instance.Running) // Simulate instance being "running" for timeout check (without actual process) // We'll test the ShouldTimeout logic directly @@ -377,7 +377,7 @@ func TestTimeoutFunctionality(t *testing.T) { } // Reset running state to avoid shutdown issues - inst.Running = false + inst.SetStatus(instance.Stopped) // Test that instance without timeout doesn't timeout noTimeoutOptions := &instance.CreateInstanceOptions{ @@ -393,7 +393,7 @@ func TestTimeoutFunctionality(t *testing.T) { } noTimeoutInst.SetTimeProvider(mockTime) - noTimeoutInst.Running = true // Set to running for timeout check + noTimeoutInst.SetStatus(instance.Running) // Set to running for timeout check noTimeoutInst.UpdateLastRequestTime() // Even with time advanced, should not timeout @@ -402,7 +402,7 @@ func TestTimeoutFunctionality(t *testing.T) { } // Reset running state to avoid shutdown issues - noTimeoutInst.Running = false + noTimeoutInst.SetStatus(instance.Stopped) } func TestConcurrentAccess(t *testing.T) { diff --git a/pkg/manager/operations.go b/pkg/manager/operations.go index 7849152..d7d06b9 100644 --- a/pkg/manager/operations.go +++ b/pkg/manager/operations.go @@ -109,7 +109,7 @@ func (im *instanceManager) UpdateInstance(name string, options *instance.CreateI } // Check if instance is running before updating options - wasRunning := instance.Running + wasRunning := instance.IsRunning() // If the instance is running, stop it first if wasRunning { @@ -147,7 +147,7 @@ func (im *instanceManager) DeleteInstance(name string) error { return fmt.Errorf("instance with name %s not found", name) } - if instance.Running { + if instance.IsRunning() { return fmt.Errorf("instance with name %s is still running, stop it before deleting", name) } @@ -173,7 +173,7 @@ func (im *instanceManager) StartInstance(name string) (*instance.Process, error) if !exists { return nil, fmt.Errorf("instance with name %s not found", name) } - if instance.Running { + if instance.IsRunning() { return instance, fmt.Errorf("instance with name %s is already running", name) } @@ -200,7 +200,7 @@ func (im *instanceManager) StopInstance(name string) (*instance.Process, error) if !exists { return nil, fmt.Errorf("instance with name %s not found", name) } - if !instance.Running { + if !instance.IsRunning() { return instance, fmt.Errorf("instance with name %s is already stopped", name) } diff --git a/pkg/server/handlers.go b/pkg/server/handlers.go index 6306a31..9c5d9a9 100644 --- a/pkg/server/handlers.go +++ b/pkg/server/handlers.go @@ -451,7 +451,7 @@ func (h *Handler) ProxyToInstance() http.HandlerFunc { return } - if !inst.Running { + if !inst.IsRunning() { http.Error(w, "Instance is not running", http.StatusServiceUnavailable) return } @@ -574,7 +574,7 @@ func (h *Handler) OpenAIProxy() http.HandlerFunc { return } - if !inst.Running { + if !inst.IsRunning() { if inst.GetOptions().OnDemandStart != nil && *inst.GetOptions().OnDemandStart { // If on-demand start is enabled, start the instance if _, err := h.InstanceManager.StartInstance(modelName); err != nil {