From 7be1f38ab6199660eee0e4ac87b2871783fc44ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Fri, 13 Feb 2026 10:48:58 +0800 Subject: [PATCH 01/12] refactor: remove AEnvCleaner interface --- api-service/main.go | 2 +- api-service/service/cleanup_service.go | 31 ++++----------------- api-service/service/cleanup_service_test.go | 2 +- 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/api-service/main.go b/api-service/main.go index 225979fe..2f0fed10 100644 --- a/api-service/main.go +++ b/api-service/main.go @@ -165,7 +165,7 @@ func main() { if err != nil { log.Fatalf("Invalid cleanup interval: %v", err) } - cleanManager := service.NewAEnvCleanManager(service.NewKubeCleaner(scheduleClient), interval) + cleanManager := service.NewAEnvCleanManager(scheduleClient, interval) go cleanManager.Start() defer cleanManager.Stop() diff --git a/api-service/service/cleanup_service.go b/api-service/service/cleanup_service.go index ff112f43..31a0d78c 100644 --- a/api-service/service/cleanup_service.go +++ b/api-service/service/cleanup_service.go @@ -22,22 +22,18 @@ import ( "time" ) -type AEnvCleaner interface { - cleanup() -} - type AEnvCleanManager struct { - cleaner AEnvCleaner + envInstanceService EnvInstanceService interval time.Duration ctx context.Context cancel context.CancelFunc } -func NewAEnvCleanManager(cleaner AEnvCleaner, duration time.Duration) *AEnvCleanManager { +func NewAEnvCleanManager(envInstanceService EnvInstanceService, duration time.Duration) *AEnvCleanManager { ctx, cancel := context.WithCancel(context.Background()) AEnvCleanManager := &AEnvCleanManager{ - cleaner: cleaner, + envInstanceService: envInstanceService, interval: duration, ctx: ctx, @@ -50,7 +46,7 @@ func NewAEnvCleanManager(cleaner AEnvCleaner, duration time.Duration) *AEnvClean func (cm *AEnvCleanManager) Start() { log.Printf("Starting cleanup service with interval: %v", cm.interval) // Execute cleanup immediately - cm.cleaner.cleanup() + _ = cm.envInstanceService.Cleanup() // Start timer ticker := time.NewTicker(cm.interval) @@ -59,7 +55,7 @@ func (cm *AEnvCleanManager) Start() { for { select { case <-ticker.C: - cm.cleaner.cleanup() + _ = cm.envInstanceService.Cleanup() case <-cm.ctx.Done(): log.Println("Cleanup service stopped") return @@ -72,20 +68,3 @@ func (cm *AEnvCleanManager) Start() { func (cm *AEnvCleanManager) Stop() { cm.cancel() } - -// KubeCleaner cleanup service responsible for periodically cleaning expired EnvInstances -type KubeCleaner struct { - scheduleClient EnvInstanceService -} - -// NewCleanupService -func NewKubeCleaner(scheduleClient EnvInstanceService) *KubeCleaner { - return &KubeCleaner{ - scheduleClient: scheduleClient, - } -} - -// cleanup executes cleanup task -func (cs *KubeCleaner) cleanup() { - _ = cs.scheduleClient.Cleanup() -} diff --git a/api-service/service/cleanup_service_test.go b/api-service/service/cleanup_service_test.go index a45c8535..f15cd84c 100644 --- a/api-service/service/cleanup_service_test.go +++ b/api-service/service/cleanup_service_test.go @@ -27,7 +27,7 @@ func TestNewCleanupService(t *testing.T) { baseURL: "http://6.1.224.11:8080", httpClient: &http.Client{Timeout: 30 * time.Second}, } - manager := NewAEnvCleanManager(NewKubeCleaner(scheduleClient), time.Minute) + manager := NewAEnvCleanManager(scheduleClient, time.Minute) manager.Start() } From 181b9241867ea7747c4b656b7469ef8e243e7c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Fri, 13 Feb 2026 14:11:38 +0800 Subject: [PATCH 02/12] fix asandbox instance create timestamp --- api-service/service/faas_client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api-service/service/faas_client.go b/api-service/service/faas_client.go index cb185d61..1cdb5db9 100644 --- a/api-service/service/faas_client.go +++ b/api-service/service/faas_client.go @@ -126,7 +126,7 @@ func (c *FaaSClient) GetEnvInstance(id string) (*models.EnvInstance, error) { IP: instance.IP, TTL: "", // No TTL field source available yet, can be added later // CreatedAt / UpdatedAt use current time or default values (should actually be returned by backend) - CreatedAt: time.Unix(instance.CreateTimestamp, 0).Format(time.RFC3339), + CreatedAt: time.UnixMilli(instance.CreateTimestamp).Format(time.RFC3339), UpdatedAt: time.Now().Format("2006-01-02 15:04:05"), Status: convertStatus(instance.Status), // Env field cannot be directly obtained from Instance, needs to rely on Create return or additional queries @@ -160,7 +160,7 @@ func (c *FaaSClient) ListEnvInstances(envName string) ([]*models.EnvInstance, er ID: inst.InstanceID, IP: inst.IP, Status: convertStatus(inst.Status), - CreatedAt: time.Now().Format("2006-01-02 15:04:05"), // Could consider constructing from CreateTimestamp + CreatedAt: time.UnixMilli(inst.CreateTimestamp).Format(time.RFC3339), // Could consider constructing from CreateTimestamp UpdatedAt: time.Now().Format("2006-01-02 15:04:05"), TTL: "", Env: nil, // Cannot obtain full Env information from Instance From f6ca5605c0b5f7350211aa583cac16124204fb9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Fri, 13 Feb 2026 14:26:42 +0800 Subject: [PATCH 03/12] feat: unified auto clean env instances in api service cleaner manager --- api-service/main.go | 3 +- api-service/middleware/metrics.go | 25 ++++++++ api-service/service/cleanup_service.go | 89 +++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/api-service/main.go b/api-service/main.go index 2f0fed10..35cbbc4d 100644 --- a/api-service/main.go +++ b/api-service/main.go @@ -165,7 +165,8 @@ func main() { if err != nil { log.Fatalf("Invalid cleanup interval: %v", err) } - cleanManager := service.NewAEnvCleanManager(scheduleClient, interval) + cleanManager := service.NewAEnvCleanManager(scheduleClient, interval). + WithMetrics(middleware.IncrementCleanupSuccess, middleware.IncrementCleanupFailure) go cleanManager.Start() defer cleanManager.Stop() diff --git a/api-service/middleware/metrics.go b/api-service/middleware/metrics.go index 62f851e4..866deeda 100644 --- a/api-service/middleware/metrics.go +++ b/api-service/middleware/metrics.go @@ -64,8 +64,33 @@ var ( }, []string{"method", "endpoint"}, ) + + // Auto cleanup metrics + cleanupSuccessCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "auto_cleanup_success_total", + Help: "Total number of successfully auto-cleaned instances", + }, + ) + + cleanupFailureCount = promauto.NewCounter( + prometheus.CounterOpts{ + Name: "auto_cleanup_failure_total", + Help: "Total number of failed auto-cleanup attempts", + }, + ) ) +// IncrementCleanupSuccess increments the cleanup success counter +func IncrementCleanupSuccess() { + cleanupSuccessCount.Inc() +} + +// IncrementCleanupFailure increments the cleanup failure counter +func IncrementCleanupFailure() { + cleanupFailureCount.Inc() +} + // MetricsMiddleware metrics collection middleware func MetricsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { diff --git a/api-service/service/cleanup_service.go b/api-service/service/cleanup_service.go index 31a0d78c..f0f2ab56 100644 --- a/api-service/service/cleanup_service.go +++ b/api-service/service/cleanup_service.go @@ -17,6 +17,7 @@ limitations under the License. package service import ( + "api-service/models" "context" "log" "time" @@ -28,6 +29,10 @@ type AEnvCleanManager struct { interval time.Duration ctx context.Context cancel context.CancelFunc + + // Metrics counters + incrementCleanupSuccess func() + incrementCleanupFailure func() } func NewAEnvCleanManager(envInstanceService EnvInstanceService, duration time.Duration) *AEnvCleanManager { @@ -38,15 +43,26 @@ func NewAEnvCleanManager(envInstanceService EnvInstanceService, duration time.Du interval: duration, ctx: ctx, cancel: cancel, + + // Default metrics functions + incrementCleanupSuccess: func() {}, + incrementCleanupFailure: func() {}, } return AEnvCleanManager } +// WithMetrics sets the metrics functions for the clean manager +func (cm *AEnvCleanManager) WithMetrics(incrementSuccess, incrementFailure func()) *AEnvCleanManager { + cm.incrementCleanupSuccess = incrementSuccess + cm.incrementCleanupFailure = incrementFailure + return cm +} + // Start starts the cleanup service func (cm *AEnvCleanManager) Start() { log.Printf("Starting cleanup service with interval: %v", cm.interval) // Execute cleanup immediately - _ = cm.envInstanceService.Cleanup() + cm.performCleanup() // Start timer ticker := time.NewTicker(cm.interval) @@ -55,7 +71,7 @@ func (cm *AEnvCleanManager) Start() { for { select { case <-ticker.C: - _ = cm.envInstanceService.Cleanup() + cm.performCleanup() case <-cm.ctx.Done(): log.Println("Cleanup service stopped") return @@ -64,6 +80,75 @@ func (cm *AEnvCleanManager) Start() { }() } +// performCleanup performs the actual cleanup task by checking TTL expiration +func (cm *AEnvCleanManager) performCleanup() { + log.Println("Starting TTL-based cleanup task...") + + // Get all environment instances + envInstances, err := cm.envInstanceService.ListEnvInstances("") + if err != nil { + log.Printf("Failed to list environment instances: %v", err) + return + } + + if len(envInstances) == 0 { + log.Println("No environment instances found") + return + } + + var deletedCount int + + // Check each instance for TTL expiration + for _, instance := range envInstances { + // Skip already terminated instances + if instance.Status == "Terminated" { + continue + } + + // Check if TTL is set and has expired + if cm.isExpired(instance) { + log.Printf("Instance %s has expired (TTL: %s), deleting...", instance.ID, instance.TTL) + err := cm.envInstanceService.DeleteEnvInstance(instance.ID) + if err != nil { + log.Printf("Failed to delete expired instance %s: %v", instance.ID, err) + cm.incrementCleanupFailure() + continue + } + deletedCount++ + cm.incrementCleanupSuccess() + log.Printf("Successfully deleted expired instance %s", instance.ID) + } + } + + log.Printf("TTL-based cleanup task completed. Deleted %d expired instances", deletedCount) +} + +// isExpired checks if an environment instance has expired based on its TTL and creation time +func (cm *AEnvCleanManager) isExpired(instance *models.EnvInstance) bool { + // If TTL is not set, consider it as non-expiring + if instance.TTL == "" { + return false + } + + // Parse TTL duration + ttlDuration, err := time.ParseDuration(instance.TTL) + if err != nil { + log.Printf("Failed to parse TTL '%s' for instance %s: %v", instance.TTL, instance.ID, err) + return false + } + + // Parse creation time + createdAt, err := time.Parse("2006-01-02 15:04:05", instance.CreatedAt) + if err != nil { + log.Printf("Failed to parse creation time '%s' for instance %s: %v", instance.CreatedAt, instance.ID, err) + return false + } + + // Check if the instance has expired + expirationTime := createdAt.Add(ttlDuration) + return time.Now().After(expirationTime) +} + // Stop stops the cleanup service func (cm *AEnvCleanManager) Stop() { cm.cancel() From 5057635462f78ffbffe66391c8a41e25513ca3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Fri, 13 Feb 2026 14:54:26 +0800 Subject: [PATCH 04/12] add ut for api-service cleanup --- api-service/service/cleanup_service_test.go | 162 +++++++++++++++++++- 1 file changed, 156 insertions(+), 6 deletions(-) diff --git a/api-service/service/cleanup_service_test.go b/api-service/service/cleanup_service_test.go index f15cd84c..c8476c18 100644 --- a/api-service/service/cleanup_service_test.go +++ b/api-service/service/cleanup_service_test.go @@ -17,17 +17,167 @@ limitations under the License. package service import ( - "net/http" + "api-service/models" + backend "envhub/models" + "errors" "testing" "time" ) func TestNewCleanupService(t *testing.T) { - scheduleClient := &ScheduleClient{ - baseURL: "http://6.1.224.11:8080", - httpClient: &http.Client{Timeout: 30 * time.Second}, + // This test is kept for compatibility but doesn't actually test anything meaningful + // since we can't easily instantiate a real ScheduleClient in tests + t.Skip("Skipping integration test that requires real ScheduleClient") +} + +// MockEnvInstanceService is a mock implementation of EnvInstanceService for testing +type MockEnvInstanceService struct { + ListEnvInstancesFunc func(envName string) ([]*models.EnvInstance, error) + DeleteEnvInstanceFunc func(id string) error +} + +func (m *MockEnvInstanceService) GetEnvInstance(id string) (*models.EnvInstance, error) { + return nil, nil +} + +func (m *MockEnvInstanceService) CreateEnvInstance(req *backend.Env) (*models.EnvInstance, error) { + return nil, nil +} + +func (m *MockEnvInstanceService) DeleteEnvInstance(id string) error { + if m.DeleteEnvInstanceFunc != nil { + return m.DeleteEnvInstanceFunc(id) + } + return nil +} + +func (m *MockEnvInstanceService) ListEnvInstances(envName string) ([]*models.EnvInstance, error) { + if m.ListEnvInstancesFunc != nil { + return m.ListEnvInstancesFunc(envName) } - manager := NewAEnvCleanManager(scheduleClient, time.Minute) + return nil, nil +} + +func (m *MockEnvInstanceService) Warmup(req *backend.Env) error { + return nil +} + +func (m *MockEnvInstanceService) Cleanup() error { + return nil +} + +// TestPerformCleanupNoInstances tests cleanup when there are no env instances +func TestPerformCleanupNoInstances(t *testing.T) { + // Create mock service that returns empty list + mockService := &MockEnvInstanceService{ + ListEnvInstancesFunc: func(envName string) ([]*models.EnvInstance, error) { + return []*models.EnvInstance{}, nil + }, + } + + // Create clean manager with mock service + manager := NewAEnvCleanManager(mockService, time.Minute) + + // Perform cleanup + manager.performCleanup() + + // Since there are no instances, no delete operations should be called + // The test passes if no panic occurs +} + +// TestPerformCleanupWithExpiredInstances tests cleanup with expired env instances +func TestPerformCleanupWithExpiredInstances(t *testing.T) { + // Create mock service with expired instances + expiredInstance := &models.EnvInstance{ + ID: "test-instance-1", + Status: "Running", + CreatedAt: "2025-01-01 10:00:00", + TTL: "1h", + } + + terminatedInstance := &models.EnvInstance{ + ID: "test-instance-2", + Status: "Terminated", + CreatedAt: "2025-01-01 10:00:00", + TTL: "1h", + } + + activeInstance := &models.EnvInstance{ + ID: "test-instance-3", + Status: "Running", + CreatedAt: time.Now().Format("2006-01-02 15:04:05"), + TTL: "1h", + } + + var deletedInstances []string + mockService := &MockEnvInstanceService{ + ListEnvInstancesFunc: func(envName string) ([]*models.EnvInstance, error) { + return []*models.EnvInstance{expiredInstance, terminatedInstance, activeInstance}, nil + }, + DeleteEnvInstanceFunc: func(id string) error { + deletedInstances = append(deletedInstances, id) + return nil + }, + } + + // Create clean manager with mock service + manager := NewAEnvCleanManager(mockService, time.Minute) + + // Perform cleanup + manager.performCleanup() + + // Verify that only the expired instance was deleted + if len(deletedInstances) != 1 { + t.Errorf("Expected 1 deleted instance, got %d", len(deletedInstances)) + } + + if len(deletedInstances) > 0 && deletedInstances[0] != "test-instance-1" { + t.Errorf("Expected deleted instance ID 'test-instance-1', got '%s'", deletedInstances[0]) + } +} + +// TestPerformCleanupWithDeleteError tests cleanup when delete operation fails +func TestPerformCleanupWithDeleteError(t *testing.T) { + // Create mock service with expired instance that fails to delete + expiredInstance := &models.EnvInstance{ + ID: "test-instance-1", + Status: "Running", + CreatedAt: "2025-01-01 10:00:00", + TTL: "1h", + } + + mockService := &MockEnvInstanceService{ + ListEnvInstancesFunc: func(envName string) ([]*models.EnvInstance, error) { + return []*models.EnvInstance{expiredInstance}, nil + }, + DeleteEnvInstanceFunc: func(id string) error { + return errors.New("delete failed") + }, + } + + // Create clean manager with mock service + manager := NewAEnvCleanManager(mockService, time.Minute) + + // Perform cleanup + manager.performCleanup() + + // The test passes if no panic occurs even when delete fails +} + +// TestPerformCleanupWithListError tests cleanup when listing instances fails +func TestPerformCleanupWithListError(t *testing.T) { + // Create mock service that fails to list instances + mockService := &MockEnvInstanceService{ + ListEnvInstancesFunc: func(envName string) ([]*models.EnvInstance, error) { + return nil, errors.New("list failed") + }, + } + + // Create clean manager with mock service + manager := NewAEnvCleanManager(mockService, time.Minute) + + // Perform cleanup + manager.performCleanup() - manager.Start() + // The test passes if no panic occurs even when list fails } From 71110e8ba6039cd2ccb1e26fea343e6dcc77e2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Fri, 13 Feb 2026 15:13:58 +0800 Subject: [PATCH 05/12] ttl support in asandbox client --- api-service/service/faas_client.go | 35 ++-------------------- api-service/service/faas_model/function.go | 7 +++++ 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/api-service/service/faas_client.go b/api-service/service/faas_client.go index 1cdb5db9..bf438eee 100644 --- a/api-service/service/faas_client.go +++ b/api-service/service/faas_client.go @@ -361,44 +361,13 @@ func (c *FaaSClient) ListInstances(labels map[string]string) (*faas_model.Instan uri := "/hapis/faas.hcs.io/v1/instances" req := &faas_model.InstanceListRequest{Labels: labels} - resp := &faas_model.APIResponse{ - Data: &faas_model.InstanceListResp{}, - } + resp := &faas_model.APIInstanceListResponse{} err := c.client.Post(uri).Body(*req).Do().Into(resp) if err != nil { return nil, fmt.Errorf("failed to list instances: %w", err) } - if !resp.Success { - return nil, fmt.Errorf("failed to list instances: %s", resp.ErrorMessage) - } - - data, ok := resp.Data.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("invalid response type for InstanceListResp") - } - - // Convert map to InstanceListResp struct - instances := []*faas_model.Instance{} - if insts, ok := data["instances"].([]interface{}); ok { - for _, inst := range insts { - if instMap, ok := inst.(map[string]interface{}); ok { - instance := &faas_model.Instance{} - if instanceID, ok := instMap["instanceID"].(string); ok { - instance.InstanceID = instanceID - } - if ip, ok := instMap["ip"].(string); ok { - instance.IP = ip - } - if status, ok := instMap["status"].(string); ok { - instance.Status = faas_model.InstanceStatus(status) - } - instances = append(instances, instance) - } - } - } - - return &faas_model.InstanceListResp{Instances: instances}, nil + return resp.Data, nil } func (c *FaaSClient) GetInstance(name string) (*faas_model.Instance, error) { diff --git a/api-service/service/faas_model/function.go b/api-service/service/faas_model/function.go index a75dae57..28e5a367 100644 --- a/api-service/service/faas_model/function.go +++ b/api-service/service/faas_model/function.go @@ -51,6 +51,7 @@ type Instance struct { IP string `json:"ip"` Labels map[string]string `json:"labels"` Status InstanceStatus `json:"status"` + TTL string `json:"ttl"` } type InstanceListResp struct { @@ -67,6 +68,12 @@ type APIResponse struct { Data interface{} `json:"data,omitempty"` } +type APIInstanceListResponse struct { + Success bool `json:"success"` + ErrorMessage string `json:"errorMessage,omitempty"` + Data *InstanceListResp `json:"data,omitempty"` +} + type RuntimeCreateOrUpdateRequest struct { Name string `json:"name"` Description string `json:"description"` From c77e8e3f609b4367fe179c68a6f6a87b306c8ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Fri, 13 Feb 2026 15:20:52 +0800 Subject: [PATCH 06/12] simplify response for asandbox --- api-service/service/faas_client.go | 34 ++++------------------ api-service/service/faas_model/function.go | 6 ++++ 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/api-service/service/faas_client.go b/api-service/service/faas_client.go index bf438eee..f043c69f 100644 --- a/api-service/service/faas_client.go +++ b/api-service/service/faas_client.go @@ -124,10 +124,10 @@ func (c *FaaSClient) GetEnvInstance(id string) (*models.EnvInstance, error) { envInst := &models.EnvInstance{ ID: instance.InstanceID, IP: instance.IP, - TTL: "", // No TTL field source available yet, can be added later + TTL: instance.TTL, // No TTL field source available yet, can be added later // CreatedAt / UpdatedAt use current time or default values (should actually be returned by backend) CreatedAt: time.UnixMilli(instance.CreateTimestamp).Format(time.RFC3339), - UpdatedAt: time.Now().Format("2006-01-02 15:04:05"), + UpdatedAt: time.Now().Format(time.RFC3339), Status: convertStatus(instance.Status), // Env field cannot be directly obtained from Instance, needs to rely on Create return or additional queries // Can only be empty here, recommend maintaining through Create/CreateFromRecord @@ -161,8 +161,8 @@ func (c *FaaSClient) ListEnvInstances(envName string) ([]*models.EnvInstance, er IP: inst.IP, Status: convertStatus(inst.Status), CreatedAt: time.UnixMilli(inst.CreateTimestamp).Format(time.RFC3339), // Could consider constructing from CreateTimestamp - UpdatedAt: time.Now().Format("2006-01-02 15:04:05"), - TTL: "", + UpdatedAt: time.Now().Format(time.RFC3339), + TTL: inst.TTL, Env: nil, // Cannot obtain full Env information from Instance }) } @@ -373,9 +373,7 @@ func (c *FaaSClient) ListInstances(labels map[string]string) (*faas_model.Instan func (c *FaaSClient) GetInstance(name string) (*faas_model.Instance, error) { uri := fmt.Sprintf("/hapis/faas.hcs.io/v1/instances/%s", name) - resp := &faas_model.APIResponse{ - Data: &faas_model.Instance{}, - } + resp := &faas_model.APIInstanceResponse{} err := c.client.Get(uri).Do().Into(resp) if err != nil { return nil, fmt.Errorf("failed to get instance %s: %w", name, err) @@ -385,27 +383,7 @@ func (c *FaaSClient) GetInstance(name string) (*faas_model.Instance, error) { return nil, fmt.Errorf("failed to get instance %s: %s", name, resp.ErrorMessage) } - data, ok := resp.Data.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("invalid response type for Instance") - } - - // Convert map to Instance struct - instance := &faas_model.Instance{} - if instanceID, ok := data["instanceID"].(string); ok { - instance.InstanceID = instanceID - } - if ip, ok := data["ip"].(string); ok { - instance.IP = ip - } - if status, ok := data["status"].(string); ok { - instance.Status = faas_model.InstanceStatus(status) - } - if createTimestamp, ok := data["createTimestamp"].(float64); ok { - instance.CreateTimestamp = int64(createTimestamp) - } - - return instance, nil + return &resp.Data, nil } func (c *FaaSClient) DeleteInstance(name string) error { diff --git a/api-service/service/faas_model/function.go b/api-service/service/faas_model/function.go index 28e5a367..94278937 100644 --- a/api-service/service/faas_model/function.go +++ b/api-service/service/faas_model/function.go @@ -68,6 +68,12 @@ type APIResponse struct { Data interface{} `json:"data,omitempty"` } +type APIInstanceResponse struct { + Success bool `json:"success"` + ErrorMessage string `json:"errorMessage,omitempty"` + Data Instance `json:"data,omitempty"` +} + type APIInstanceListResponse struct { Success bool `json:"success"` ErrorMessage string `json:"errorMessage,omitempty"` From 5d2d796fa79a9df134b3cf02bc537a41ac78687b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Wed, 25 Feb 2026 18:34:15 +0800 Subject: [PATCH 07/12] feat: add TTL support for environment instance management - Add GetTTL() method to Env model to read TTL value from DeployConfig - Update CreateInstanceByFunction to accept TTL parameter from request - Refactor InitializeFunction API to use request body for parameters - Add FunctionInitializeOptions struct for initialization options This enables TTL-based instance lifecycle management through FaaS backend. --- api-service/service/faas_client.go | 24 ++++++++-------------- api-service/service/faas_model/function.go | 6 ++++++ envhub/models/env.go | 21 +++++++++++++++++++ 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/api-service/service/faas_client.go b/api-service/service/faas_client.go index f043c69f..1af9392f 100644 --- a/api-service/service/faas_client.go +++ b/api-service/service/faas_client.go @@ -40,7 +40,7 @@ func (c *FaaSClient) CreateEnvInstance(req *backend.Env) (*models.EnvInstance, e // return nil, fmt.Errorf("prepare function failed: %v", err.Error()) //} // Synchronously call the function - instanceId, err := c.CreateInstanceByFunction(functionName, dynamicRuntimeName) + instanceId, err := c.CreateInstanceByFunction(functionName, dynamicRuntimeName, req.GetTTL()) if err != nil { return nil, fmt.Errorf("failed to create env instance %s: %v", functionName, err) } @@ -99,13 +99,16 @@ func (c *FaaSClient) PrepareFunction(functionName string, req *backend.Env) erro return nil } -func (c *FaaSClient) CreateInstanceByFunction(name string, dynamicRuntimeName string) (string, error) { +func (c *FaaSClient) CreateInstanceByFunction(name string, dynamicRuntimeName string, ttl int64) (string, error) { f, err := c.GetFunction(name) if err != nil { return "", err } - instanceId, err := c.InitializeFunction(f.Name, dynamicRuntimeName, faas_model.FunctionInvocationTypeSync, []byte("{}")) + instanceId, err := c.InitializeFunction(f.Name, faas_model.FunctionInitializeOptions{ + DynamicRuntimeName: dynamicRuntimeName, + TTL: ttl, + }) if err != nil { return "", fmt.Errorf("failed to create functions instance from faas server: %v", err.Error()) } @@ -323,7 +326,7 @@ func (c *FaaSClient) GetRuntime(name string) (*faas_model.Runtime, error) { return runtime, nil } -func (c *FaaSClient) InitializeFunction(name string, dynamicRuntimeName string, invocationType string, invocationBody []byte) (string, error) { +func (c *FaaSClient) InitializeFunction(name string, initOptions faas_model.FunctionInitializeOptions) (string, error) { uri := fmt.Sprintf("/hapis/faas.hcs.io/v1/functions/%s/initialize", name) f, err := c.GetFunction(name) @@ -331,18 +334,7 @@ func (c *FaaSClient) InitializeFunction(name string, dynamicRuntimeName string, return "", err } - if invocationType == faas_model.FunctionInvocationTypeAsync { - invocationType = faas_model.FunctionInvocationTypeAsync - } else { - invocationType = faas_model.FunctionInvocationTypeSync - } - - req := c.client.Post(uri).BodyData(invocationBody).Timeout(time.Duration(f.Timeout)*time.Second).Query("invocationType", invocationType) - - // If dynamicRuntimeName is provided, add it to the query parameters - if dynamicRuntimeName != "" { - req = req.Query("dynamicRuntimeName", dynamicRuntimeName) - } + req := c.client.Post(uri).Body(initOptions).Timeout(time.Duration(f.Timeout) * time.Second) resp, err := req.Do().Response() if err != nil { diff --git a/api-service/service/faas_model/function.go b/api-service/service/faas_model/function.go index 94278937..48c566b3 100644 --- a/api-service/service/faas_model/function.go +++ b/api-service/service/faas_model/function.go @@ -108,3 +108,9 @@ const ( RuntimeStatusPreparing RuntimeStatus = "preparing" RuntimeStatusError RuntimeStatus = "error" ) + +type FunctionInitializeOptions struct { + // DynamicRuntimeName 动态运行时名称,可选参数 + DynamicRuntimeName string `json:"dynamicRuntimeName,omitempty"` + TTL int64 `json:"ttl,omitempty"` +} diff --git a/envhub/models/env.go b/envhub/models/env.go index 59739c61..a97162d7 100644 --- a/envhub/models/env.go +++ b/envhub/models/env.go @@ -243,3 +243,24 @@ func (e *Env) GetCPU() string { } return "" } + +// GetTTL retrieves the ttl configuration from DeployConfig, returns 0 if not exists or invalid +func (e *Env) GetTTL() int64 { + if val, exists := e.DeployConfig["ttl"]; exists { + switch v := val.(type) { + case int64: + return v + case int: + return int64(v) + case float64: + return int64(v) + case string: + // Try to parse string to int64 + var ttl int64 + if _, err := fmt.Sscanf(v, "%d", &ttl); err == nil { + return ttl + } + } + } + return 0 +} From 354c466b10e85c7132153ba806786c64d6c56917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Thu, 26 Feb 2026 16:10:55 +0800 Subject: [PATCH 08/12] remove invalid label selector --- deploy/redis/templates/_helpers.tpl | 1 - 1 file changed, 1 deletion(-) diff --git a/deploy/redis/templates/_helpers.tpl b/deploy/redis/templates/_helpers.tpl index 6749c734..662c0826 100644 --- a/deploy/redis/templates/_helpers.tpl +++ b/deploy/redis/templates/_helpers.tpl @@ -20,7 +20,6 @@ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} {{- define "redis.selectorLabels" -}} -app.kubernetes.io/instance: {{ .Release.Name }} {{- if .Values.global.selectorLabels }} {{ tpl (toYaml .Values.global.selectorLabels) . }} {{- end }} From 27814f19d3b96bd9b169ad3374f5791ab9f863b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Thu, 26 Feb 2026 16:12:55 +0800 Subject: [PATCH 09/12] refactor: change TTL field type from string to int64 - Change EnvInstance.TTL from string to int64 (seconds) - Update Instance.TTL in faas_model to int64 - Update PodListResponseData.TTL to int64 - Refactor cleanup_service.isExpired() to use int64 TTL: - Check TTL <= 0 instead of empty string - Use time.Duration(instance.TTL) * time.Second directly - Remove time.ParseDuration parsing - Update logging format from %s to %d for TTL - Update test files to use int64 seconds instead of duration strings This simplifies TTL handling by using seconds as the standard unit instead of duration strings. --- api-service/models/env_instance.go | 2 +- api-service/service/cleanup_service.go | 14 +++++--------- api-service/service/cleanup_service_test.go | 8 ++++---- api-service/service/faas_client.go | 15 ++++++--------- api-service/service/faas_model/function.go | 2 +- api-service/service/schedule_client.go | 8 +++++++- 6 files changed, 24 insertions(+), 25 deletions(-) diff --git a/api-service/models/env_instance.go b/api-service/models/env_instance.go index db364ffa..9bc54790 100644 --- a/api-service/models/env_instance.go +++ b/api-service/models/env_instance.go @@ -58,7 +58,7 @@ type EnvInstance struct { CreatedAt string `json:"created_at"` // Creation time UpdatedAt string `json:"updated_at"` // Update time IP string `json:"ip"` // Instance IP - TTL string `json:"ttl"` // time to live + TTL int64 `json:"ttl"` // time to live in seconds Owner string `json:"owner"` // Instance owner (user who created it) } diff --git a/api-service/service/cleanup_service.go b/api-service/service/cleanup_service.go index f0f2ab56..d2bb50ed 100644 --- a/api-service/service/cleanup_service.go +++ b/api-service/service/cleanup_service.go @@ -107,7 +107,7 @@ func (cm *AEnvCleanManager) performCleanup() { // Check if TTL is set and has expired if cm.isExpired(instance) { - log.Printf("Instance %s has expired (TTL: %s), deleting...", instance.ID, instance.TTL) + log.Printf("Instance %s has expired (TTL: %d seconds), deleting...", instance.ID, instance.TTL) err := cm.envInstanceService.DeleteEnvInstance(instance.ID) if err != nil { log.Printf("Failed to delete expired instance %s: %v", instance.ID, err) @@ -125,17 +125,13 @@ func (cm *AEnvCleanManager) performCleanup() { // isExpired checks if an environment instance has expired based on its TTL and creation time func (cm *AEnvCleanManager) isExpired(instance *models.EnvInstance) bool { - // If TTL is not set, consider it as non-expiring - if instance.TTL == "" { + // If TTL is not set (0 or negative), consider it as non-expiring + if instance.TTL <= 0 { return false } - // Parse TTL duration - ttlDuration, err := time.ParseDuration(instance.TTL) - if err != nil { - log.Printf("Failed to parse TTL '%s' for instance %s: %v", instance.TTL, instance.ID, err) - return false - } + // TTL is in seconds, convert to duration + ttlDuration := time.Duration(instance.TTL) * time.Second // Parse creation time createdAt, err := time.Parse("2006-01-02 15:04:05", instance.CreatedAt) diff --git a/api-service/service/cleanup_service_test.go b/api-service/service/cleanup_service_test.go index c8476c18..6b6c28b0 100644 --- a/api-service/service/cleanup_service_test.go +++ b/api-service/service/cleanup_service_test.go @@ -92,21 +92,21 @@ func TestPerformCleanupWithExpiredInstances(t *testing.T) { ID: "test-instance-1", Status: "Running", CreatedAt: "2025-01-01 10:00:00", - TTL: "1h", + TTL: 3600, // 1 hour in seconds } terminatedInstance := &models.EnvInstance{ ID: "test-instance-2", Status: "Terminated", CreatedAt: "2025-01-01 10:00:00", - TTL: "1h", + TTL: 3600, // 1 hour in seconds } activeInstance := &models.EnvInstance{ ID: "test-instance-3", Status: "Running", CreatedAt: time.Now().Format("2006-01-02 15:04:05"), - TTL: "1h", + TTL: 3600, // 1 hour in seconds } var deletedInstances []string @@ -143,7 +143,7 @@ func TestPerformCleanupWithDeleteError(t *testing.T) { ID: "test-instance-1", Status: "Running", CreatedAt: "2025-01-01 10:00:00", - TTL: "1h", + TTL: 3600, // 1 hour in seconds } mockService := &MockEnvInstanceService{ diff --git a/api-service/service/faas_client.go b/api-service/service/faas_client.go index 1af9392f..66b8cf86 100644 --- a/api-service/service/faas_client.go +++ b/api-service/service/faas_client.go @@ -125,16 +125,13 @@ func (c *FaaSClient) GetEnvInstance(id string) (*models.EnvInstance, error) { // Map model.Instance -> models.EnvInstance envInst := &models.EnvInstance{ - ID: instance.InstanceID, - IP: instance.IP, - TTL: instance.TTL, // No TTL field source available yet, can be added later - // CreatedAt / UpdatedAt use current time or default values (should actually be returned by backend) + ID: instance.InstanceID, + IP: instance.IP, + TTL: instance.TTL, CreatedAt: time.UnixMilli(instance.CreateTimestamp).Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339), Status: convertStatus(instance.Status), - // Env field cannot be directly obtained from Instance, needs to rely on Create return or additional queries - // Can only be empty here, recommend maintaining through Create/CreateFromRecord - Env: nil, + Env: nil, } return envInst, nil @@ -163,10 +160,10 @@ func (c *FaaSClient) ListEnvInstances(envName string) ([]*models.EnvInstance, er ID: inst.InstanceID, IP: inst.IP, Status: convertStatus(inst.Status), - CreatedAt: time.UnixMilli(inst.CreateTimestamp).Format(time.RFC3339), // Could consider constructing from CreateTimestamp + CreatedAt: time.UnixMilli(inst.CreateTimestamp).Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339), TTL: inst.TTL, - Env: nil, // Cannot obtain full Env information from Instance + Env: nil, }) } diff --git a/api-service/service/faas_model/function.go b/api-service/service/faas_model/function.go index 48c566b3..16bd0344 100644 --- a/api-service/service/faas_model/function.go +++ b/api-service/service/faas_model/function.go @@ -51,7 +51,7 @@ type Instance struct { IP string `json:"ip"` Labels map[string]string `json:"labels"` Status InstanceStatus `json:"status"` - TTL string `json:"ttl"` + TTL int64 `json:"ttl"` } type InstanceListResp struct { diff --git a/api-service/service/schedule_client.go b/api-service/service/schedule_client.go index 1541e3e1..e11cbeeb 100644 --- a/api-service/service/schedule_client.go +++ b/api-service/service/schedule_client.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "net/http" + "strconv" "time" log "github.com/sirupsen/logrus" @@ -588,6 +589,11 @@ func (c *ScheduleClient) ListEnvInstances(envName string) ([]*models.EnvInstance // Format CreatedAt time createdAtStr := podData.CreatedAt.Format("2006-01-02 15:04:05") nowStr := time.Now().Format("2006-01-02 15:04:05") + ttl, err := strconv.ParseInt(podData.TTL, 10, 64) + if err != nil { + log.Warnf("Failed to parse TTL value '%v': %v, setting to 0", podData.TTL, err) + ttl = 0 + } instances[i] = &models.EnvInstance{ ID: podData.ID, @@ -596,7 +602,7 @@ func (c *ScheduleClient) ListEnvInstances(envName string) ([]*models.EnvInstance CreatedAt: createdAtStr, UpdatedAt: nowStr, IP: podData.IP, - TTL: podData.TTL, + TTL: ttl, Owner: podData.Owner, } } From ab8d743ce13f20327215200f771231f4367fae36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Thu, 26 Feb 2026 19:23:04 +0800 Subject: [PATCH 10/12] Revert "refactor: change TTL field type from string to int64" This reverts commit 27814f19d3b96bd9b169ad3374f5791ab9f863b1. --- api-service/models/env_instance.go | 2 +- api-service/service/cleanup_service.go | 14 +++++++++----- api-service/service/cleanup_service_test.go | 8 ++++---- api-service/service/faas_client.go | 15 +++++++++------ api-service/service/faas_model/function.go | 2 +- api-service/service/schedule_client.go | 8 +------- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/api-service/models/env_instance.go b/api-service/models/env_instance.go index 9bc54790..db364ffa 100644 --- a/api-service/models/env_instance.go +++ b/api-service/models/env_instance.go @@ -58,7 +58,7 @@ type EnvInstance struct { CreatedAt string `json:"created_at"` // Creation time UpdatedAt string `json:"updated_at"` // Update time IP string `json:"ip"` // Instance IP - TTL int64 `json:"ttl"` // time to live in seconds + TTL string `json:"ttl"` // time to live Owner string `json:"owner"` // Instance owner (user who created it) } diff --git a/api-service/service/cleanup_service.go b/api-service/service/cleanup_service.go index d2bb50ed..f0f2ab56 100644 --- a/api-service/service/cleanup_service.go +++ b/api-service/service/cleanup_service.go @@ -107,7 +107,7 @@ func (cm *AEnvCleanManager) performCleanup() { // Check if TTL is set and has expired if cm.isExpired(instance) { - log.Printf("Instance %s has expired (TTL: %d seconds), deleting...", instance.ID, instance.TTL) + log.Printf("Instance %s has expired (TTL: %s), deleting...", instance.ID, instance.TTL) err := cm.envInstanceService.DeleteEnvInstance(instance.ID) if err != nil { log.Printf("Failed to delete expired instance %s: %v", instance.ID, err) @@ -125,13 +125,17 @@ func (cm *AEnvCleanManager) performCleanup() { // isExpired checks if an environment instance has expired based on its TTL and creation time func (cm *AEnvCleanManager) isExpired(instance *models.EnvInstance) bool { - // If TTL is not set (0 or negative), consider it as non-expiring - if instance.TTL <= 0 { + // If TTL is not set, consider it as non-expiring + if instance.TTL == "" { return false } - // TTL is in seconds, convert to duration - ttlDuration := time.Duration(instance.TTL) * time.Second + // Parse TTL duration + ttlDuration, err := time.ParseDuration(instance.TTL) + if err != nil { + log.Printf("Failed to parse TTL '%s' for instance %s: %v", instance.TTL, instance.ID, err) + return false + } // Parse creation time createdAt, err := time.Parse("2006-01-02 15:04:05", instance.CreatedAt) diff --git a/api-service/service/cleanup_service_test.go b/api-service/service/cleanup_service_test.go index 6b6c28b0..c8476c18 100644 --- a/api-service/service/cleanup_service_test.go +++ b/api-service/service/cleanup_service_test.go @@ -92,21 +92,21 @@ func TestPerformCleanupWithExpiredInstances(t *testing.T) { ID: "test-instance-1", Status: "Running", CreatedAt: "2025-01-01 10:00:00", - TTL: 3600, // 1 hour in seconds + TTL: "1h", } terminatedInstance := &models.EnvInstance{ ID: "test-instance-2", Status: "Terminated", CreatedAt: "2025-01-01 10:00:00", - TTL: 3600, // 1 hour in seconds + TTL: "1h", } activeInstance := &models.EnvInstance{ ID: "test-instance-3", Status: "Running", CreatedAt: time.Now().Format("2006-01-02 15:04:05"), - TTL: 3600, // 1 hour in seconds + TTL: "1h", } var deletedInstances []string @@ -143,7 +143,7 @@ func TestPerformCleanupWithDeleteError(t *testing.T) { ID: "test-instance-1", Status: "Running", CreatedAt: "2025-01-01 10:00:00", - TTL: 3600, // 1 hour in seconds + TTL: "1h", } mockService := &MockEnvInstanceService{ diff --git a/api-service/service/faas_client.go b/api-service/service/faas_client.go index 66b8cf86..1af9392f 100644 --- a/api-service/service/faas_client.go +++ b/api-service/service/faas_client.go @@ -125,13 +125,16 @@ func (c *FaaSClient) GetEnvInstance(id string) (*models.EnvInstance, error) { // Map model.Instance -> models.EnvInstance envInst := &models.EnvInstance{ - ID: instance.InstanceID, - IP: instance.IP, - TTL: instance.TTL, + ID: instance.InstanceID, + IP: instance.IP, + TTL: instance.TTL, // No TTL field source available yet, can be added later + // CreatedAt / UpdatedAt use current time or default values (should actually be returned by backend) CreatedAt: time.UnixMilli(instance.CreateTimestamp).Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339), Status: convertStatus(instance.Status), - Env: nil, + // Env field cannot be directly obtained from Instance, needs to rely on Create return or additional queries + // Can only be empty here, recommend maintaining through Create/CreateFromRecord + Env: nil, } return envInst, nil @@ -160,10 +163,10 @@ func (c *FaaSClient) ListEnvInstances(envName string) ([]*models.EnvInstance, er ID: inst.InstanceID, IP: inst.IP, Status: convertStatus(inst.Status), - CreatedAt: time.UnixMilli(inst.CreateTimestamp).Format(time.RFC3339), + CreatedAt: time.UnixMilli(inst.CreateTimestamp).Format(time.RFC3339), // Could consider constructing from CreateTimestamp UpdatedAt: time.Now().Format(time.RFC3339), TTL: inst.TTL, - Env: nil, + Env: nil, // Cannot obtain full Env information from Instance }) } diff --git a/api-service/service/faas_model/function.go b/api-service/service/faas_model/function.go index 16bd0344..48c566b3 100644 --- a/api-service/service/faas_model/function.go +++ b/api-service/service/faas_model/function.go @@ -51,7 +51,7 @@ type Instance struct { IP string `json:"ip"` Labels map[string]string `json:"labels"` Status InstanceStatus `json:"status"` - TTL int64 `json:"ttl"` + TTL string `json:"ttl"` } type InstanceListResp struct { diff --git a/api-service/service/schedule_client.go b/api-service/service/schedule_client.go index e11cbeeb..1541e3e1 100644 --- a/api-service/service/schedule_client.go +++ b/api-service/service/schedule_client.go @@ -23,7 +23,6 @@ import ( "fmt" "io" "net/http" - "strconv" "time" log "github.com/sirupsen/logrus" @@ -589,11 +588,6 @@ func (c *ScheduleClient) ListEnvInstances(envName string) ([]*models.EnvInstance // Format CreatedAt time createdAtStr := podData.CreatedAt.Format("2006-01-02 15:04:05") nowStr := time.Now().Format("2006-01-02 15:04:05") - ttl, err := strconv.ParseInt(podData.TTL, 10, 64) - if err != nil { - log.Warnf("Failed to parse TTL value '%v': %v, setting to 0", podData.TTL, err) - ttl = 0 - } instances[i] = &models.EnvInstance{ ID: podData.ID, @@ -602,7 +596,7 @@ func (c *ScheduleClient) ListEnvInstances(envName string) ([]*models.EnvInstance CreatedAt: createdAtStr, UpdatedAt: nowStr, IP: podData.IP, - TTL: ttl, + TTL: podData.TTL, Owner: podData.Owner, } } From a2db82de8e1b3e0cc64f42d0cf317936205e270d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Thu, 26 Feb 2026 19:46:21 +0800 Subject: [PATCH 11/12] rollback faas api TTL field type from int64 to string --- api-service/service/faas_client.go | 4 ++-- api-service/service/faas_model/function.go | 2 +- envhub/models/env.go | 22 ++++++++++------------ 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/api-service/service/faas_client.go b/api-service/service/faas_client.go index 1af9392f..3c937fca 100644 --- a/api-service/service/faas_client.go +++ b/api-service/service/faas_client.go @@ -99,7 +99,7 @@ func (c *FaaSClient) PrepareFunction(functionName string, req *backend.Env) erro return nil } -func (c *FaaSClient) CreateInstanceByFunction(name string, dynamicRuntimeName string, ttl int64) (string, error) { +func (c *FaaSClient) CreateInstanceByFunction(name string, dynamicRuntimeName string, ttl string) (string, error) { f, err := c.GetFunction(name) if err != nil { return "", err @@ -354,7 +354,7 @@ func (c *FaaSClient) ListInstances(labels map[string]string) (*faas_model.Instan req := &faas_model.InstanceListRequest{Labels: labels} resp := &faas_model.APIInstanceListResponse{} - err := c.client.Post(uri).Body(*req).Do().Into(resp) + err := c.client.Get(uri).Body(*req).Do().Into(resp) if err != nil { return nil, fmt.Errorf("failed to list instances: %w", err) } diff --git a/api-service/service/faas_model/function.go b/api-service/service/faas_model/function.go index 48c566b3..a849ea7d 100644 --- a/api-service/service/faas_model/function.go +++ b/api-service/service/faas_model/function.go @@ -112,5 +112,5 @@ const ( type FunctionInitializeOptions struct { // DynamicRuntimeName 动态运行时名称,可选参数 DynamicRuntimeName string `json:"dynamicRuntimeName,omitempty"` - TTL int64 `json:"ttl,omitempty"` + TTL string `json:"ttl,omitempty"` } diff --git a/envhub/models/env.go b/envhub/models/env.go index a97162d7..beaab481 100644 --- a/envhub/models/env.go +++ b/envhub/models/env.go @@ -244,23 +244,21 @@ func (e *Env) GetCPU() string { return "" } -// GetTTL retrieves the ttl configuration from DeployConfig, returns 0 if not exists or invalid -func (e *Env) GetTTL() int64 { +// GetTTL retrieves the ttl configuration from DeployConfig, returns empty string if not exists +func (e *Env) GetTTL() string { if val, exists := e.DeployConfig["ttl"]; exists { switch v := val.(type) { - case int64: + case string: return v + case int64: + return fmt.Sprintf("%d", v) case int: - return int64(v) + return fmt.Sprintf("%d", v) case float64: - return int64(v) - case string: - // Try to parse string to int64 - var ttl int64 - if _, err := fmt.Sscanf(v, "%d", &ttl); err == nil { - return ttl - } + return fmt.Sprintf("%.0f", v) + default: + return fmt.Sprintf("%v", v) } } - return 0 + return "" } From 65e597e4a460200b90d97c7dfae22dbf924f6c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=8F=E6=96=87?= Date: Thu, 26 Feb 2026 20:58:11 +0800 Subject: [PATCH 12/12] feat: add cleanupInterval configuration and improve time parsing - Add --cleanup-interval argument to api-service deployment - Add cleanupInterval value to api-service/values.yaml (default: 5m) - Improve time parsing in cleanup_service.isExpired(): - Use time.DateTime constant instead of hardcoded format string - Add fallback to time.RFC3339 for compatibility - Add comprehensive unit tests for time parsing: - TestIsExpiredWithDateTimeFormat: tests DateTime format parsing - TestIsExpiredWithRFC3339Format: tests RFC3339 fallback parsing - TestIsExpiredWithInvalidTimeFormat: tests invalid time format handling - TestIsExpiredWithInvalidTTLFormat: tests invalid TTL format handling - TestIsExpiredWithEmptyTTL: tests empty TTL handling - TestIsExpiredWithVariousTTLDurations: tests various TTL durations All tests pass successfully. --- api-service/service/cleanup_service.go | 12 +- api-service/service/cleanup_service_test.go | 173 +++++++++++++++++++ api-service/service/schedule_client.go | 4 +- deploy/api-service/templates/deployment.yaml | 2 + deploy/api-service/values.yaml | 3 + 5 files changed, 188 insertions(+), 6 deletions(-) diff --git a/api-service/service/cleanup_service.go b/api-service/service/cleanup_service.go index f0f2ab56..db957f4b 100644 --- a/api-service/service/cleanup_service.go +++ b/api-service/service/cleanup_service.go @@ -137,11 +137,15 @@ func (cm *AEnvCleanManager) isExpired(instance *models.EnvInstance) bool { return false } - // Parse creation time - createdAt, err := time.Parse("2006-01-02 15:04:05", instance.CreatedAt) + // Parse creation time using time.DateTime format (2006-01-02 15:04:05) + createdAt, err := time.Parse(time.DateTime, instance.CreatedAt) if err != nil { - log.Printf("Failed to parse creation time '%s' for instance %s: %v", instance.CreatedAt, instance.ID, err) - return false + // Fallback to RFC3339 if DateTime parsing fails + createdAt, err = time.Parse(time.RFC3339, instance.CreatedAt) + if err != nil { + log.Printf("Failed to parse creation time '%s' for instance %s: %v", instance.CreatedAt, instance.ID, err) + return false + } } // Check if the instance has expired diff --git a/api-service/service/cleanup_service_test.go b/api-service/service/cleanup_service_test.go index c8476c18..c31071f6 100644 --- a/api-service/service/cleanup_service_test.go +++ b/api-service/service/cleanup_service_test.go @@ -181,3 +181,176 @@ func TestPerformCleanupWithListError(t *testing.T) { // The test passes if no panic occurs even when list fails } + +// TestIsExpiredWithDateTimeFormat tests isExpired with time.DateTime format +func TestIsExpiredWithDateTimeFormat(t *testing.T) { + // Create instance with DateTime format creation time + expiredInstance := &models.EnvInstance{ + ID: "test-expired-1", + Status: "Running", + CreatedAt: "2025-01-01 10:00:00", // time.DateTime format + TTL: "1h", + } + + activeInstance := &models.EnvInstance{ + ID: "test-active-1", + Status: "Running", + CreatedAt: time.Now().Format(time.DateTime), + TTL: "1h", + } + + mockService := &MockEnvInstanceService{} + manager := NewAEnvCleanManager(mockService, time.Minute) + + // Test expired instance + if !manager.isExpired(expiredInstance) { + t.Errorf("Expected instance %s to be expired", expiredInstance.ID) + } + + // Test active instance + if manager.isExpired(activeInstance) { + t.Errorf("Expected instance %s to be active", activeInstance.ID) + } +} + +// TestIsExpiredWithRFC3339Format tests isExpired with RFC3339 format (fallback) +func TestIsExpiredWithRFC3339Format(t *testing.T) { + // Create instance with RFC3339 format creation time + expiredInstance := &models.EnvInstance{ + ID: "test-expired-rfc3339-1", + Status: "Running", + CreatedAt: "2025-01-01T10:00:00+08:00", // RFC3339 format + TTL: "1h", + } + + activeInstance := &models.EnvInstance{ + ID: "test-active-rfc3339-1", + Status: "Running", + CreatedAt: time.Now().Format(time.RFC3339), + TTL: "1h", + } + + mockService := &MockEnvInstanceService{} + manager := NewAEnvCleanManager(mockService, time.Minute) + + // Test expired instance + if !manager.isExpired(expiredInstance) { + t.Errorf("Expected instance %s to be expired", expiredInstance.ID) + } + + // Test active instance + if manager.isExpired(activeInstance) { + t.Errorf("Expected instance %s to be active", activeInstance.ID) + } +} + +// TestIsExpiredWithInvalidTimeFormat tests isExpired with invalid time format +func TestIsExpiredWithInvalidTimeFormat(t *testing.T) { + // Create instance with invalid time format + invalidInstance := &models.EnvInstance{ + ID: "test-invalid-time", + Status: "Running", + CreatedAt: "invalid-time-format", + TTL: "1h", + } + + mockService := &MockEnvInstanceService{} + manager := NewAEnvCleanManager(mockService, time.Minute) + + // Should return false for invalid time format + if manager.isExpired(invalidInstance) { + t.Errorf("Expected instance %s with invalid time format to not be expired", invalidInstance.ID) + } +} + +// TestIsExpiredWithInvalidTTLFormat tests isExpired with invalid TTL format +func TestIsExpiredWithInvalidTTLFormat(t *testing.T) { + // Create instance with invalid TTL format + invalidInstance := &models.EnvInstance{ + ID: "test-invalid-ttl", + Status: "Running", + CreatedAt: time.Now().Format(time.DateTime), + TTL: "invalid-ttl", + } + + mockService := &MockEnvInstanceService{} + manager := NewAEnvCleanManager(mockService, time.Minute) + + // Should return false for invalid TTL format + if manager.isExpired(invalidInstance) { + t.Errorf("Expected instance %s with invalid TTL format to not be expired", invalidInstance.ID) + } +} + +// TestIsExpiredWithEmptyTTL tests isExpired with empty TTL +func TestIsExpiredWithEmptyTTL(t *testing.T) { + // Create instance with empty TTL + emptyTTLInstance := &models.EnvInstance{ + ID: "test-empty-ttl", + Status: "Running", + CreatedAt: "2025-01-01 10:00:00", + TTL: "", + } + + mockService := &MockEnvInstanceService{} + manager := NewAEnvCleanManager(mockService, time.Minute) + + // Should return false for empty TTL + if manager.isExpired(emptyTTLInstance) { + t.Errorf("Expected instance %s with empty TTL to not be expired", emptyTTLInstance.ID) + } +} + +// TestIsExpiredWithVariousTTLDurations tests isExpired with various TTL durations +func TestIsExpiredWithVariousTTLDurations(t *testing.T) { + testCases := []struct { + name string + createdAt string + ttl string + expected bool + }{ + { + name: "expired with seconds", + createdAt: "2025-01-01 10:00:00", + ttl: "30s", + expected: true, + }, + { + name: "expired with minutes", + createdAt: "2025-01-01 10:00:00", + ttl: "5m", + expected: true, + }, + { + name: "expired with hours", + createdAt: "2025-01-01 10:00:00", + ttl: "2h", + expected: true, + }, + { + name: "active with long TTL", + createdAt: time.Now().Format(time.DateTime), + ttl: "24h", + expected: false, + }, + } + + mockService := &MockEnvInstanceService{} + manager := NewAEnvCleanManager(mockService, time.Minute) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + instance := &models.EnvInstance{ + ID: "test-" + tc.name, + Status: "Running", + CreatedAt: tc.createdAt, + TTL: tc.ttl, + } + + result := manager.isExpired(instance) + if result != tc.expected { + t.Errorf("Expected isExpired to be %v, got %v for instance %s", tc.expected, result, instance.ID) + } + }) + } +} diff --git a/api-service/service/schedule_client.go b/api-service/service/schedule_client.go index 1541e3e1..327f28c5 100644 --- a/api-service/service/schedule_client.go +++ b/api-service/service/schedule_client.go @@ -586,8 +586,8 @@ func (c *ScheduleClient) ListEnvInstances(envName string) ([]*models.EnvInstance } // Format CreatedAt time - createdAtStr := podData.CreatedAt.Format("2006-01-02 15:04:05") - nowStr := time.Now().Format("2006-01-02 15:04:05") + createdAtStr := podData.CreatedAt.Format(time.RFC3339) + nowStr := time.Now().Format(time.RFC3339) instances[i] = &models.EnvInstance{ ID: podData.ID, diff --git a/deploy/api-service/templates/deployment.yaml b/deploy/api-service/templates/deployment.yaml index eddc9375..aac0ee3c 100644 --- a/deploy/api-service/templates/deployment.yaml +++ b/deploy/api-service/templates/deployment.yaml @@ -43,6 +43,8 @@ spec: - {{ include "api-service.scheduleType" . }} - --qps - {{ include "api-service.qps" . | quote }} + - --cleanup-interval + - {{ .Values.cleanupInterval }} ports: - name: http containerPort: {{ .Values.service.port }} diff --git a/deploy/api-service/values.yaml b/deploy/api-service/values.yaml index 07b0fd0d..cb46d4b7 100644 --- a/deploy/api-service/values.yaml +++ b/deploy/api-service/values.yaml @@ -59,3 +59,6 @@ autoscaling: nodeSelector: {} tolerations: {} + +# Cleanup service configuration +cleanupInterval: "5m"