package services import ( "context" "encoding/json" "fmt" "time" "go.uber.org/zap" "github.com/RyanCopley/skybridge/faas/internal/domain" "github.com/RyanCopley/skybridge/faas/internal/repository" "github.com/google/uuid" ) type executionService struct { executionRepo repository.ExecutionRepository functionRepo repository.FunctionRepository runtimeService RuntimeService logger *zap.Logger } func NewExecutionService( executionRepo repository.ExecutionRepository, functionRepo repository.FunctionRepository, runtimeService RuntimeService, logger *zap.Logger, ) ExecutionService { return &executionService{ executionRepo: executionRepo, functionRepo: functionRepo, runtimeService: runtimeService, logger: logger, } } func (s *executionService) Execute(ctx context.Context, req *domain.ExecuteFunctionRequest, userID string) (*domain.ExecuteFunctionResponse, error) { s.logger.Info("Executing function", zap.String("function_id", req.FunctionID.String()), zap.String("user_id", userID), zap.Bool("async", req.Async)) // Get function definition function, err := s.functionRepo.GetByID(ctx, req.FunctionID) if err != nil { return nil, fmt.Errorf("function not found: %w", err) } // Create execution record // Initialize input with empty JSON if nil or empty input := req.Input if input == nil || len(input) == 0 { input = json.RawMessage(`{}`) } execution := &domain.FunctionExecution{ ID: uuid.New(), FunctionID: req.FunctionID, Status: domain.StatusPending, Input: input, Output: json.RawMessage(`{}`), // Initialize with empty JSON object ExecutorID: userID, CreatedAt: time.Now(), } // Store execution createdExecution, err := s.executionRepo.Create(ctx, execution) if err != nil { s.logger.Error("Failed to create execution record", zap.String("function_id", req.FunctionID.String()), zap.Error(err)) return nil, fmt.Errorf("failed to create execution record: %w", err) } if req.Async { // Start async execution go s.executeAsync(context.Background(), createdExecution, function) return &domain.ExecuteFunctionResponse{ ExecutionID: createdExecution.ID, Status: domain.StatusPending, }, nil } else { // Execute synchronously return s.executeSync(ctx, createdExecution, function) } } func (s *executionService) executeSync(ctx context.Context, execution *domain.FunctionExecution, function *domain.FunctionDefinition) (*domain.ExecuteFunctionResponse, error) { // Update status to running execution.Status = domain.StatusRunning execution.StartedAt = &[]time.Time{time.Now()}[0] if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil { s.logger.Warn("Failed to update execution status to running", zap.Error(err)) } // Get runtime backend backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime)) if err != nil { execution.Status = domain.StatusFailed execution.Error = fmt.Sprintf("failed to get runtime backend: %v", err) s.updateExecutionComplete(ctx, execution) return &domain.ExecuteFunctionResponse{ ExecutionID: execution.ID, Status: domain.StatusFailed, Error: execution.Error, }, nil } // Execute function result, err := backend.Execute(ctx, function, execution.Input) if err != nil { execution.Status = domain.StatusFailed execution.Error = fmt.Sprintf("execution failed: %v", err) s.updateExecutionComplete(ctx, execution) return &domain.ExecuteFunctionResponse{ ExecutionID: execution.ID, Status: domain.StatusFailed, Error: execution.Error, }, nil } // Update execution with results execution.Status = domain.StatusCompleted // Handle empty output if len(result.Output) == 0 { execution.Output = json.RawMessage(`{}`) } else { execution.Output = result.Output } execution.Error = result.Error execution.Duration = result.Duration execution.MemoryUsed = result.MemoryUsed s.updateExecutionComplete(ctx, execution) if result.Error != "" { execution.Status = domain.StatusFailed } return &domain.ExecuteFunctionResponse{ ExecutionID: execution.ID, Status: execution.Status, Output: execution.Output, Error: execution.Error, Duration: execution.Duration, MemoryUsed: execution.MemoryUsed, }, nil } func (s *executionService) executeAsync(ctx context.Context, execution *domain.FunctionExecution, function *domain.FunctionDefinition) { // Update status to running execution.Status = domain.StatusRunning execution.StartedAt = &[]time.Time{time.Now()}[0] if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil { s.logger.Warn("Failed to update execution status to running", zap.Error(err)) } // Get runtime backend backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime)) if err != nil { s.logger.Error("Failed to get runtime backend for async execution", zap.String("execution_id", execution.ID.String()), zap.Error(err)) execution.Status = domain.StatusFailed execution.Error = fmt.Sprintf("failed to get runtime backend: %v", err) s.updateExecutionComplete(ctx, execution) return } // Execute function result, err := backend.Execute(ctx, function, execution.Input) if err != nil { s.logger.Error("Async function execution failed", zap.String("execution_id", execution.ID.String()), zap.Error(err)) execution.Status = domain.StatusFailed execution.Error = fmt.Sprintf("execution failed: %v", err) s.updateExecutionComplete(ctx, execution) return } // Update execution with results execution.Status = domain.StatusCompleted // Handle empty output if len(result.Output) == 0 { execution.Output = json.RawMessage(`{}`) } else { execution.Output = result.Output } execution.Error = result.Error execution.Duration = result.Duration execution.MemoryUsed = result.MemoryUsed if result.Error != "" { execution.Status = domain.StatusFailed } s.updateExecutionComplete(ctx, execution) s.logger.Info("Async function execution completed", zap.String("execution_id", execution.ID.String()), zap.String("status", string(execution.Status)), zap.Duration("duration", execution.Duration)) } func (s *executionService) updateExecutionComplete(ctx context.Context, execution *domain.FunctionExecution) { execution.CompletedAt = &[]time.Time{time.Now()}[0] if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil { s.logger.Error("Failed to update execution completion", zap.String("execution_id", execution.ID.String()), zap.Error(err)) } } func (s *executionService) GetByID(ctx context.Context, id uuid.UUID) (*domain.FunctionExecution, error) { execution, err := s.executionRepo.GetByID(ctx, id) if err != nil { return nil, fmt.Errorf("execution not found: %w", err) } return execution, nil } func (s *executionService) List(ctx context.Context, functionID *uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error) { if limit <= 0 { limit = 50 // Default limit } if limit > 100 { limit = 100 // Max limit } return s.executionRepo.List(ctx, functionID, limit, offset) } func (s *executionService) GetByFunctionID(ctx context.Context, functionID uuid.UUID, limit, offset int) ([]*domain.FunctionExecution, error) { if limit <= 0 { limit = 50 // Default limit } if limit > 100 { limit = 100 // Max limit } return s.executionRepo.GetByFunctionID(ctx, functionID, limit, offset) } func (s *executionService) Cancel(ctx context.Context, id uuid.UUID, userID string) error { s.logger.Info("Canceling execution", zap.String("execution_id", id.String()), zap.String("user_id", userID)) // Get execution execution, err := s.executionRepo.GetByID(ctx, id) if err != nil { return fmt.Errorf("execution not found: %w", err) } // Check if execution is still running if execution.Status != domain.StatusRunning && execution.Status != domain.StatusPending { return fmt.Errorf("execution is not running (status: %s)", execution.Status) } // Get function to determine runtime function, err := s.functionRepo.GetByID(ctx, execution.FunctionID) if err != nil { return fmt.Errorf("function not found: %w", err) } // Stop execution in runtime backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime)) if err != nil { return fmt.Errorf("failed to get runtime backend: %w", err) } if err := backend.StopExecution(ctx, id); err != nil { s.logger.Warn("Failed to stop execution in runtime", zap.String("execution_id", id.String()), zap.Error(err)) } // Update execution status execution.Status = domain.StatusCanceled execution.Error = "execution canceled by user" execution.CompletedAt = &[]time.Time{time.Now()}[0] if _, err := s.executionRepo.Update(ctx, execution.ID, execution); err != nil { return fmt.Errorf("failed to update execution status: %w", err) } s.logger.Info("Execution canceled successfully", zap.String("execution_id", id.String())) return nil } func (s *executionService) GetLogs(ctx context.Context, id uuid.UUID) ([]string, error) { // Get execution execution, err := s.executionRepo.GetByID(ctx, id) if err != nil { return nil, fmt.Errorf("execution not found: %w", err) } // Get function to determine runtime function, err := s.functionRepo.GetByID(ctx, execution.FunctionID) if err != nil { return nil, fmt.Errorf("function not found: %w", err) } // Get runtime backend backend, err := s.runtimeService.GetBackend(ctx, string(function.Runtime)) if err != nil { return nil, fmt.Errorf("failed to get runtime backend: %w", err) } // Get logs from runtime logs, err := backend.GetLogs(ctx, id) if err != nil { return nil, fmt.Errorf("failed to get logs: %w", err) } return logs, nil } func (s *executionService) GetRunningExecutions(ctx context.Context) ([]*domain.FunctionExecution, error) { return s.executionRepo.GetRunningExecutions(ctx) }