@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for Temporal service
|
||||
set -e
|
||||
|
||||
echo "Building Temporal service..."
|
||||
|
||||
# Change to temporal-service directory
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Initialize Go module if not already done
|
||||
if [ ! -f "go.sum" ]; then
|
||||
echo "Initializing Go module..."
|
||||
go mod tidy
|
||||
fi
|
||||
|
||||
# Build the service
|
||||
echo "Compiling Go binary..."
|
||||
go build -o temporal-service main.go
|
||||
|
||||
# Make it executable
|
||||
chmod +x temporal-service
|
||||
|
||||
echo "Build completed successfully!"
|
||||
echo "Binary location: $(pwd)/temporal-service"
|
||||
echo ""
|
||||
echo "Prerequisites:"
|
||||
echo " 1. Install Temporal CLI: brew install temporal"
|
||||
echo " 2. Start Temporal server: temporal server start-dev"
|
||||
echo ""
|
||||
echo "To run the service:"
|
||||
echo " ./temporal-service"
|
||||
echo ""
|
||||
echo "Environment variables:"
|
||||
echo " PORT - HTTP port (default: 8080)"
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Example usage script for the Temporal service
|
||||
set -e
|
||||
|
||||
echo "Temporal Service Example Usage"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
|
||||
# Check if service is running
|
||||
if ! curl -s http://localhost:8080/health > /dev/null; then
|
||||
echo "Starting Temporal service..."
|
||||
echo "Please run in another terminal: ./temporal-service"
|
||||
echo "Then run this script again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Temporal service is running"
|
||||
echo ""
|
||||
|
||||
# Create example recipe
|
||||
RECIPE_FILE="/tmp/example-recipe.yaml"
|
||||
cat > $RECIPE_FILE << EOF
|
||||
version: "1.0.0"
|
||||
title: "Daily Report Generator"
|
||||
description: "Generates a daily report"
|
||||
prompt: |
|
||||
Generate a daily report with the following information:
|
||||
- Current date and time
|
||||
- System status
|
||||
- Recent activity summary
|
||||
|
||||
Please format the output as a structured report.
|
||||
EOF
|
||||
|
||||
echo "Created example recipe: $RECIPE_FILE"
|
||||
echo ""
|
||||
|
||||
# Function to make API calls
|
||||
make_api_call() {
|
||||
local action="$1"
|
||||
local job_id="$2"
|
||||
local cron="$3"
|
||||
local recipe_path="$4"
|
||||
|
||||
local payload="{\"action\": \"$action\""
|
||||
|
||||
if [ -n "$job_id" ]; then
|
||||
payload="$payload, \"job_id\": \"$job_id\""
|
||||
fi
|
||||
|
||||
if [ -n "$cron" ]; then
|
||||
payload="$payload, \"cron\": \"$cron\""
|
||||
fi
|
||||
|
||||
if [ -n "$recipe_path" ]; then
|
||||
payload="$payload, \"recipe_path\": \"$recipe_path\""
|
||||
fi
|
||||
|
||||
payload="$payload}"
|
||||
|
||||
echo "API Call: $payload"
|
||||
curl -s -X POST http://localhost:8080/jobs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" | jq .
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Example 1: Create a daily job
|
||||
echo "1. Creating a daily job (runs at 9 AM every day)..."
|
||||
make_api_call "create" "daily-report" "0 9 * * *" "$RECIPE_FILE"
|
||||
|
||||
# Example 2: Create an hourly job
|
||||
echo "2. Creating an hourly job..."
|
||||
make_api_call "create" "hourly-check" "0 * * * *" "$RECIPE_FILE"
|
||||
|
||||
# Example 3: List all jobs
|
||||
echo "3. Listing all scheduled jobs..."
|
||||
make_api_call "list"
|
||||
|
||||
# Example 4: Pause a job
|
||||
echo "4. Pausing the hourly job..."
|
||||
make_api_call "pause" "hourly-check"
|
||||
|
||||
# Example 5: List jobs again to see paused status
|
||||
echo "5. Listing jobs to see paused status..."
|
||||
make_api_call "list"
|
||||
|
||||
# Example 6: Unpause the job
|
||||
echo "6. Unpausing the hourly job..."
|
||||
make_api_call "unpause" "hourly-check"
|
||||
|
||||
# Example 7: Run a job immediately
|
||||
echo "7. Running daily-report job immediately..."
|
||||
echo "Note: This will fail without goose-scheduler-executor binary"
|
||||
make_api_call "run_now" "daily-report"
|
||||
|
||||
# Example 8: Delete jobs
|
||||
echo "8. Cleaning up - deleting jobs..."
|
||||
make_api_call "delete" "daily-report"
|
||||
make_api_call "delete" "hourly-check"
|
||||
|
||||
# Example 9: Final list (should be empty)
|
||||
echo "9. Final job list (should be empty)..."
|
||||
make_api_call "list"
|
||||
|
||||
# Clean up
|
||||
rm -f $RECIPE_FILE
|
||||
|
||||
echo "Example completed!"
|
||||
echo ""
|
||||
echo "Common cron expressions:"
|
||||
echo " '0 9 * * *' - Daily at 9 AM"
|
||||
echo " '0 */6 * * *' - Every 6 hours"
|
||||
echo " '*/15 * * * *' - Every 15 minutes"
|
||||
echo " '0 0 * * 0' - Weekly on Sunday at midnight"
|
||||
echo " '0 0 1 * *' - Monthly on the 1st at midnight"
|
||||
@@ -1,35 +0,0 @@
|
||||
module temporal-service
|
||||
|
||||
go 1.21
|
||||
|
||||
require go.temporal.io/sdk v1.24.0
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
|
||||
github.com/gogo/googleapis v1.4.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gogo/status v1.1.1 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/pborman/uuid v1.2.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/robfig/cron v1.2.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
go.temporal.io/api v1.24.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect
|
||||
google.golang.org/grpc v1.57.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,544 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go.temporal.io/sdk/activity"
|
||||
"go.temporal.io/sdk/client"
|
||||
"go.temporal.io/sdk/temporal"
|
||||
"go.temporal.io/sdk/worker"
|
||||
"go.temporal.io/sdk/workflow"
|
||||
)
|
||||
|
||||
const (
|
||||
TaskQueueName = "goose-task-queue"
|
||||
Namespace = "default"
|
||||
)
|
||||
|
||||
// Global service instance for activities to access
|
||||
var globalService *TemporalService
|
||||
|
||||
// Request/Response types for HTTP API
|
||||
type JobRequest struct {
|
||||
Action string `json:"action"` // create, delete, pause, unpause, list, run_now
|
||||
JobID string `json:"job_id"`
|
||||
CronExpr string `json:"cron"`
|
||||
RecipePath string `json:"recipe_path"`
|
||||
}
|
||||
|
||||
type JobResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Jobs []JobStatus `json:"jobs,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type JobStatus struct {
|
||||
ID string `json:"id"`
|
||||
CronExpr string `json:"cron"`
|
||||
RecipePath string `json:"recipe_path"`
|
||||
LastRun *string `json:"last_run,omitempty"`
|
||||
NextRun *string `json:"next_run,omitempty"`
|
||||
CurrentlyRunning bool `json:"currently_running"`
|
||||
Paused bool `json:"paused"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type RunNowResponse struct {
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
// TemporalService manages the Temporal client and provides HTTP API
|
||||
type TemporalService struct {
|
||||
client client.Client
|
||||
worker worker.Worker
|
||||
scheduleJobs map[string]*JobStatus // In-memory job tracking
|
||||
runningJobs map[string]bool // Track which jobs are currently running
|
||||
}
|
||||
|
||||
// NewTemporalService creates a new Temporal service that connects to existing server
|
||||
func NewTemporalService() (*TemporalService, error) {
|
||||
// Create client (assumes Temporal server is already running)
|
||||
c, err := client.Dial(client.Options{
|
||||
HostPort: "127.0.0.1:7233",
|
||||
Namespace: Namespace,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temporal client: %w", err)
|
||||
}
|
||||
|
||||
// Create worker
|
||||
w := worker.New(c, TaskQueueName, worker.Options{})
|
||||
w.RegisterWorkflow(GooseJobWorkflow)
|
||||
w.RegisterActivity(ExecuteGooseRecipe)
|
||||
|
||||
if err := w.Start(); err != nil {
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("failed to start worker: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Connected to Temporal server successfully")
|
||||
|
||||
service := &TemporalService{
|
||||
client: c,
|
||||
worker: w,
|
||||
scheduleJobs: make(map[string]*JobStatus),
|
||||
runningJobs: make(map[string]bool),
|
||||
}
|
||||
|
||||
// Set global service for activities
|
||||
globalService = service
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down the Temporal service
|
||||
func (ts *TemporalService) Stop() {
|
||||
log.Println("Shutting down Temporal service...")
|
||||
if ts.worker != nil {
|
||||
ts.worker.Stop()
|
||||
}
|
||||
if ts.client != nil {
|
||||
ts.client.Close()
|
||||
}
|
||||
log.Println("Temporal service stopped")
|
||||
}
|
||||
|
||||
// Workflow definition for executing Goose recipes
|
||||
func GooseJobWorkflow(ctx workflow.Context, jobID, recipePath string) (string, error) {
|
||||
logger := workflow.GetLogger(ctx)
|
||||
logger.Info("Starting Goose job workflow", "jobID", jobID, "recipePath", recipePath)
|
||||
|
||||
ao := workflow.ActivityOptions{
|
||||
StartToCloseTimeout: 2 * time.Hour, // Allow up to 2 hours for job execution
|
||||
RetryPolicy: &temporal.RetryPolicy{
|
||||
InitialInterval: time.Second,
|
||||
BackoffCoefficient: 2.0,
|
||||
MaximumInterval: time.Minute,
|
||||
MaximumAttempts: 3,
|
||||
NonRetryableErrorTypes: []string{"InvalidRecipeError"},
|
||||
},
|
||||
}
|
||||
ctx = workflow.WithActivityOptions(ctx, ao)
|
||||
|
||||
var sessionID string
|
||||
err := workflow.ExecuteActivity(ctx, ExecuteGooseRecipe, jobID, recipePath).Get(ctx, &sessionID)
|
||||
if err != nil {
|
||||
logger.Error("Goose job workflow failed", "jobID", jobID, "error", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
logger.Info("Goose job workflow completed", "jobID", jobID, "sessionID", sessionID)
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
// Activity definition for executing Goose recipes
|
||||
func ExecuteGooseRecipe(ctx context.Context, jobID, recipePath string) (string, error) {
|
||||
logger := activity.GetLogger(ctx)
|
||||
logger.Info("Executing Goose recipe", "jobID", jobID, "recipePath", recipePath)
|
||||
|
||||
// Mark job as running at the start
|
||||
if globalService != nil {
|
||||
globalService.markJobAsRunning(jobID)
|
||||
// Ensure we mark it as not running when we're done
|
||||
defer globalService.markJobAsNotRunning(jobID)
|
||||
}
|
||||
|
||||
// Check if recipe file exists
|
||||
if _, err := os.Stat(recipePath); os.IsNotExist(err) {
|
||||
return "", temporal.NewNonRetryableApplicationError(
|
||||
fmt.Sprintf("recipe file not found: %s", recipePath),
|
||||
"InvalidRecipeError",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
// Execute the Goose recipe via the executor binary
|
||||
cmd := exec.CommandContext(ctx, "goose-scheduler-executor", jobID, recipePath)
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("GOOSE_JOB_ID=%s", jobID))
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
logger.Error("Recipe execution failed", "jobID", jobID, "stderr", string(exitError.Stderr))
|
||||
return "", fmt.Errorf("recipe execution failed: %s", string(exitError.Stderr))
|
||||
}
|
||||
return "", fmt.Errorf("failed to execute recipe: %w", err)
|
||||
}
|
||||
|
||||
sessionID := strings.TrimSpace(string(output))
|
||||
logger.Info("Recipe executed successfully", "jobID", jobID, "sessionID", sessionID)
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
// HTTP API handlers
|
||||
|
||||
func (ts *TemporalService) handleJobs(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
ts.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
var req JobRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
ts.writeErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("Invalid JSON: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
var resp JobResponse
|
||||
|
||||
switch req.Action {
|
||||
case "create":
|
||||
resp = ts.createSchedule(req)
|
||||
case "delete":
|
||||
resp = ts.deleteSchedule(req)
|
||||
case "pause":
|
||||
resp = ts.pauseSchedule(req)
|
||||
case "unpause":
|
||||
resp = ts.unpauseSchedule(req)
|
||||
case "list":
|
||||
resp = ts.listSchedules()
|
||||
case "run_now":
|
||||
resp = ts.runNow(req)
|
||||
default:
|
||||
resp = JobResponse{Success: false, Message: fmt.Sprintf("Unknown action: %s", req.Action)}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func (ts *TemporalService) createSchedule(req JobRequest) JobResponse {
|
||||
if req.JobID == "" || req.CronExpr == "" || req.RecipePath == "" {
|
||||
return JobResponse{Success: false, Message: "Missing required fields: job_id, cron, recipe_path"}
|
||||
}
|
||||
|
||||
// Check if job already exists
|
||||
if _, exists := ts.scheduleJobs[req.JobID]; exists {
|
||||
return JobResponse{Success: false, Message: fmt.Sprintf("Job with ID '%s' already exists", req.JobID)}
|
||||
}
|
||||
|
||||
// Validate recipe file exists
|
||||
if _, err := os.Stat(req.RecipePath); os.IsNotExist(err) {
|
||||
return JobResponse{Success: false, Message: fmt.Sprintf("Recipe file not found: %s", req.RecipePath)}
|
||||
}
|
||||
|
||||
scheduleID := fmt.Sprintf("goose-job-%s", req.JobID)
|
||||
|
||||
// Create Temporal schedule
|
||||
schedule := client.ScheduleOptions{
|
||||
ID: scheduleID,
|
||||
Spec: client.ScheduleSpec{
|
||||
CronExpressions: []string{req.CronExpr},
|
||||
},
|
||||
Action: &client.ScheduleWorkflowAction{
|
||||
ID: fmt.Sprintf("workflow-%s-{{.ScheduledTime.Unix}}", req.JobID),
|
||||
Workflow: GooseJobWorkflow,
|
||||
Args: []interface{}{req.JobID, req.RecipePath},
|
||||
TaskQueue: TaskQueueName,
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := ts.client.ScheduleClient().Create(ctx, schedule)
|
||||
if err != nil {
|
||||
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to create schedule: %v", err)}
|
||||
}
|
||||
|
||||
// Track job in memory
|
||||
jobStatus := &JobStatus{
|
||||
ID: req.JobID,
|
||||
CronExpr: req.CronExpr,
|
||||
RecipePath: req.RecipePath,
|
||||
CurrentlyRunning: false,
|
||||
Paused: false,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
ts.scheduleJobs[req.JobID] = jobStatus
|
||||
|
||||
log.Printf("Created schedule for job: %s", req.JobID)
|
||||
return JobResponse{Success: true, Message: "Schedule created successfully"}
|
||||
}
|
||||
|
||||
func (ts *TemporalService) deleteSchedule(req JobRequest) JobResponse {
|
||||
if req.JobID == "" {
|
||||
return JobResponse{Success: false, Message: "Missing job_id"}
|
||||
}
|
||||
|
||||
scheduleID := fmt.Sprintf("goose-job-%s", req.JobID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
handle := ts.client.ScheduleClient().GetHandle(ctx, scheduleID)
|
||||
err := handle.Delete(ctx)
|
||||
if err != nil {
|
||||
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to delete schedule: %v", err)}
|
||||
}
|
||||
|
||||
// Remove from memory
|
||||
delete(ts.scheduleJobs, req.JobID)
|
||||
|
||||
log.Printf("Deleted schedule for job: %s", req.JobID)
|
||||
return JobResponse{Success: true, Message: "Schedule deleted successfully"}
|
||||
}
|
||||
|
||||
func (ts *TemporalService) pauseSchedule(req JobRequest) JobResponse {
|
||||
if req.JobID == "" {
|
||||
return JobResponse{Success: false, Message: "Missing job_id"}
|
||||
}
|
||||
|
||||
scheduleID := fmt.Sprintf("goose-job-%s", req.JobID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
handle := ts.client.ScheduleClient().GetHandle(ctx, scheduleID)
|
||||
err := handle.Pause(ctx, client.SchedulePauseOptions{
|
||||
Note: "Paused via API",
|
||||
})
|
||||
if err != nil {
|
||||
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to pause schedule: %v", err)}
|
||||
}
|
||||
|
||||
// Update in memory
|
||||
if job, exists := ts.scheduleJobs[req.JobID]; exists {
|
||||
job.Paused = true
|
||||
}
|
||||
|
||||
log.Printf("Paused schedule for job: %s", req.JobID)
|
||||
return JobResponse{Success: true, Message: "Schedule paused successfully"}
|
||||
}
|
||||
|
||||
func (ts *TemporalService) unpauseSchedule(req JobRequest) JobResponse {
|
||||
if req.JobID == "" {
|
||||
return JobResponse{Success: false, Message: "Missing job_id"}
|
||||
}
|
||||
|
||||
scheduleID := fmt.Sprintf("goose-job-%s", req.JobID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
handle := ts.client.ScheduleClient().GetHandle(ctx, scheduleID)
|
||||
err := handle.Unpause(ctx, client.ScheduleUnpauseOptions{
|
||||
Note: "Unpaused via API",
|
||||
})
|
||||
if err != nil {
|
||||
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to unpause schedule: %v", err)}
|
||||
}
|
||||
|
||||
// Update in memory
|
||||
if job, exists := ts.scheduleJobs[req.JobID]; exists {
|
||||
job.Paused = false
|
||||
}
|
||||
|
||||
log.Printf("Unpaused schedule for job: %s", req.JobID)
|
||||
return JobResponse{Success: true, Message: "Schedule unpaused successfully"}
|
||||
}
|
||||
|
||||
func (ts *TemporalService) listSchedules() JobResponse {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// List all schedules from Temporal
|
||||
iter, err := ts.client.ScheduleClient().List(ctx, client.ScheduleListOptions{})
|
||||
if err != nil {
|
||||
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to list schedules: %v", err)}
|
||||
}
|
||||
|
||||
var jobs []JobStatus
|
||||
for iter.HasNext() {
|
||||
schedule, err := iter.Next()
|
||||
if err != nil {
|
||||
log.Printf("Error listing schedules: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract job ID from schedule ID
|
||||
if strings.HasPrefix(schedule.ID, "goose-job-") {
|
||||
jobID := strings.TrimPrefix(schedule.ID, "goose-job-")
|
||||
|
||||
// Get additional details from in-memory tracking
|
||||
var jobStatus JobStatus
|
||||
if tracked, exists := ts.scheduleJobs[jobID]; exists {
|
||||
jobStatus = *tracked
|
||||
} else {
|
||||
// Fallback for schedules not in memory
|
||||
jobStatus = JobStatus{
|
||||
ID: jobID,
|
||||
CreatedAt: time.Now(), // We don't have the real creation time
|
||||
}
|
||||
}
|
||||
|
||||
// Update with Temporal schedule info
|
||||
if len(schedule.Spec.CronExpressions) > 0 {
|
||||
jobStatus.CronExpr = schedule.Spec.CronExpressions[0]
|
||||
}
|
||||
|
||||
// Get detailed schedule information including paused state and running status
|
||||
scheduleHandle := ts.client.ScheduleClient().GetHandle(ctx, schedule.ID)
|
||||
if desc, err := scheduleHandle.Describe(ctx); err == nil {
|
||||
jobStatus.Paused = desc.Schedule.State.Paused
|
||||
|
||||
// Check if there are any running workflows for this job
|
||||
jobStatus.CurrentlyRunning = ts.isJobCurrentlyRunning(ctx, jobID)
|
||||
|
||||
// Update last run time if available
|
||||
if len(desc.Info.RecentActions) > 0 {
|
||||
lastAction := desc.Info.RecentActions[len(desc.Info.RecentActions)-1]
|
||||
if !lastAction.ActualTime.IsZero() {
|
||||
lastRunStr := lastAction.ActualTime.Format(time.RFC3339)
|
||||
jobStatus.LastRun = &lastRunStr
|
||||
}
|
||||
}
|
||||
|
||||
// Update next run time if available - this field may not exist in older SDK versions
|
||||
// We'll skip this for now to avoid compilation errors
|
||||
} else {
|
||||
log.Printf("Warning: Could not get detailed info for schedule %s: %v", schedule.ID, err)
|
||||
}
|
||||
|
||||
// Update in-memory tracking with latest info
|
||||
ts.scheduleJobs[jobID] = &jobStatus
|
||||
|
||||
jobs = append(jobs, jobStatus)
|
||||
}
|
||||
}
|
||||
|
||||
return JobResponse{Success: true, Jobs: jobs}
|
||||
}
|
||||
|
||||
// isJobCurrentlyRunning checks if there are any running workflows for the given job ID
|
||||
func (ts *TemporalService) isJobCurrentlyRunning(ctx context.Context, jobID string) bool {
|
||||
// Check our in-memory tracking of running jobs
|
||||
if running, exists := ts.runningJobs[jobID]; exists && running {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// markJobAsRunning sets a job as currently running
|
||||
func (ts *TemporalService) markJobAsRunning(jobID string) {
|
||||
ts.runningJobs[jobID] = true
|
||||
log.Printf("Marked job %s as running", jobID)
|
||||
}
|
||||
|
||||
// markJobAsNotRunning sets a job as not currently running
|
||||
func (ts *TemporalService) markJobAsNotRunning(jobID string) {
|
||||
delete(ts.runningJobs, jobID)
|
||||
log.Printf("Marked job %s as not running", jobID)
|
||||
}
|
||||
|
||||
func (ts *TemporalService) runNow(req JobRequest) JobResponse {
|
||||
if req.JobID == "" {
|
||||
return JobResponse{Success: false, Message: "Missing job_id"}
|
||||
}
|
||||
|
||||
// Get job details
|
||||
job, exists := ts.scheduleJobs[req.JobID]
|
||||
if !exists {
|
||||
return JobResponse{Success: false, Message: fmt.Sprintf("Job '%s' not found", req.JobID)}
|
||||
}
|
||||
|
||||
// Execute workflow immediately
|
||||
workflowOptions := client.StartWorkflowOptions{
|
||||
ID: fmt.Sprintf("manual-%s-%d", req.JobID, time.Now().Unix()),
|
||||
TaskQueue: TaskQueueName,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
we, err := ts.client.ExecuteWorkflow(ctx, workflowOptions, GooseJobWorkflow, req.JobID, job.RecipePath)
|
||||
if err != nil {
|
||||
return JobResponse{Success: false, Message: fmt.Sprintf("Failed to start workflow: %v", err)}
|
||||
}
|
||||
|
||||
// Don't wait for completion in run_now, just return the workflow ID
|
||||
log.Printf("Manual execution started for job: %s, workflow: %s", req.JobID, we.GetID())
|
||||
return JobResponse{
|
||||
Success: true,
|
||||
Message: "Job execution started",
|
||||
Data: RunNowResponse{SessionID: we.GetID()}, // Return workflow ID as session ID for now
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TemporalService) writeErrorResponse(w http.ResponseWriter, statusCode int, message string) {
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(JobResponse{Success: false, Message: message})
|
||||
}
|
||||
|
||||
func (ts *TemporalService) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
|
||||
}
|
||||
|
||||
func main() {
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
log.Println("Starting Temporal service...")
|
||||
log.Println("Note: This service requires a running Temporal server at 127.0.0.1:7233")
|
||||
log.Println("Start Temporal server with: temporal server start-dev")
|
||||
|
||||
// Create Temporal service
|
||||
service, err := NewTemporalService()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Temporal service: %v", err)
|
||||
}
|
||||
|
||||
// Set up HTTP server
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/jobs", service.handleJobs)
|
||||
mux.HandleFunc("/health", service.handleHealth)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-sigChan
|
||||
log.Println("Received shutdown signal")
|
||||
|
||||
// Shutdown HTTP server
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
server.Shutdown(ctx)
|
||||
|
||||
// Stop Temporal service
|
||||
service.Stop()
|
||||
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
log.Printf("Temporal service starting on port %s", port)
|
||||
log.Printf("Health endpoint: http://localhost:%s/health", port)
|
||||
log.Printf("Jobs endpoint: http://localhost:%s/jobs", port)
|
||||
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("HTTP server failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Startup script for Temporal service with integrated Temporal server
|
||||
set -e
|
||||
|
||||
echo "Starting Temporal development environment..."
|
||||
|
||||
# Check if temporal CLI is available
|
||||
if ! command -v temporal &> /dev/null; then
|
||||
echo "Error: Temporal CLI not found!"
|
||||
echo "Please install it first:"
|
||||
echo " brew install temporal"
|
||||
echo " # or download from https://github.com/temporalio/cli/releases"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if temporal-service binary exists
|
||||
if [ ! -f "./temporal-service" ]; then
|
||||
echo "Error: temporal-service binary not found!"
|
||||
echo "Please build it first: ./build.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set data directory
|
||||
DATA_DIR="${GOOSE_DATA_DIR:-./data}"
|
||||
mkdir -p "$DATA_DIR"
|
||||
|
||||
echo "Data directory: $DATA_DIR"
|
||||
echo "Starting Temporal server..."
|
||||
|
||||
# Start Temporal server in background
|
||||
temporal server start-dev \
|
||||
--db-filename "$DATA_DIR/temporal.db" \
|
||||
--port 7233 \
|
||||
--ui-port 8233 \
|
||||
--log-level warn &
|
||||
|
||||
TEMPORAL_PID=$!
|
||||
echo "Temporal server started with PID: $TEMPORAL_PID"
|
||||
|
||||
# Function to cleanup on exit
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "Shutting down..."
|
||||
if [ ! -z "$SERVICE_PID" ]; then
|
||||
echo "Stopping temporal-service (PID: $SERVICE_PID)..."
|
||||
kill $SERVICE_PID 2>/dev/null || true
|
||||
fi
|
||||
echo "Stopping Temporal server (PID: $TEMPORAL_PID)..."
|
||||
kill $TEMPORAL_PID 2>/dev/null || true
|
||||
wait $TEMPORAL_PID 2>/dev/null || true
|
||||
echo "Shutdown complete"
|
||||
}
|
||||
|
||||
# Set trap for cleanup
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# Wait for Temporal server to be ready
|
||||
echo "Waiting for Temporal server to be ready..."
|
||||
for i in {1..30}; do
|
||||
if curl -s http://localhost:7233/api/v1/namespaces > /dev/null 2>&1; then
|
||||
echo "Temporal server is ready!"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo "Error: Temporal server failed to start within 30 seconds"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Start the temporal service
|
||||
echo "Starting temporal-service..."
|
||||
PORT="${PORT:-8080}" ./temporal-service &
|
||||
SERVICE_PID=$!
|
||||
|
||||
echo ""
|
||||
echo "🎉 Temporal development environment is running!"
|
||||
echo ""
|
||||
echo "Services:"
|
||||
echo " - Temporal Server: http://localhost:7233 (gRPC)"
|
||||
echo " - Temporal Web UI: http://localhost:8233"
|
||||
echo " - Goose Scheduler API: http://localhost:${PORT:-8080}"
|
||||
echo ""
|
||||
echo "API Endpoints:"
|
||||
echo " - Health: http://localhost:${PORT:-8080}/health"
|
||||
echo " - Jobs: http://localhost:${PORT:-8080}/jobs"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop all services"
|
||||
|
||||
# Wait for the service to exit
|
||||
wait $SERVICE_PID
|
||||
Binary file not shown.
@@ -1,123 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for Temporal service
|
||||
set -e
|
||||
|
||||
echo "Testing Temporal service..."
|
||||
|
||||
# Check if service is running
|
||||
if ! curl -s http://localhost:8080/health > /dev/null; then
|
||||
echo "Error: Temporal service is not running on port 8080"
|
||||
echo "Please start it with: ./temporal-service"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Service is running"
|
||||
|
||||
# Test health endpoint
|
||||
echo "Testing health endpoint..."
|
||||
HEALTH_RESPONSE=$(curl -s http://localhost:8080/health)
|
||||
if [[ $HEALTH_RESPONSE == *"healthy"* ]]; then
|
||||
echo "✓ Health check passed"
|
||||
else
|
||||
echo "✗ Health check failed: $HEALTH_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test list schedules (should be empty initially)
|
||||
echo "Testing list schedules..."
|
||||
LIST_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action": "list"}')
|
||||
|
||||
if [[ $LIST_RESPONSE == *"\"success\":true"* ]]; then
|
||||
echo "✓ List schedules works"
|
||||
else
|
||||
echo "✗ List schedules failed: $LIST_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create a test recipe file
|
||||
TEST_RECIPE="/tmp/test-recipe.yaml"
|
||||
cat > $TEST_RECIPE << EOF
|
||||
version: "1.0.0"
|
||||
title: "Test Recipe"
|
||||
description: "A test recipe for the scheduler"
|
||||
prompt: "This is a test prompt for scheduled execution."
|
||||
EOF
|
||||
|
||||
echo "Created test recipe at $TEST_RECIPE"
|
||||
|
||||
# Test create schedule
|
||||
echo "Testing create schedule..."
|
||||
CREATE_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"action\": \"create\", \"job_id\": \"test-job\", \"cron\": \"0 */6 * * *\", \"recipe_path\": \"$TEST_RECIPE\"}")
|
||||
|
||||
if [[ $CREATE_RESPONSE == *"\"success\":true"* ]]; then
|
||||
echo "✓ Create schedule works"
|
||||
else
|
||||
echo "✗ Create schedule failed: $CREATE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test list schedules again (should have one job)
|
||||
echo "Testing list schedules with job..."
|
||||
LIST_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action": "list"}')
|
||||
|
||||
if [[ $LIST_RESPONSE == *"test-job"* ]]; then
|
||||
echo "✓ Job appears in list"
|
||||
else
|
||||
echo "✗ Job not found in list: $LIST_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test pause schedule
|
||||
echo "Testing pause schedule..."
|
||||
PAUSE_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action": "pause", "job_id": "test-job"}')
|
||||
|
||||
if [[ $PAUSE_RESPONSE == *"\"success\":true"* ]]; then
|
||||
echo "✓ Pause schedule works"
|
||||
else
|
||||
echo "✗ Pause schedule failed: $PAUSE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test unpause schedule
|
||||
echo "Testing unpause schedule..."
|
||||
UNPAUSE_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action": "unpause", "job_id": "test-job"}')
|
||||
|
||||
if [[ $UNPAUSE_RESPONSE == *"\"success\":true"* ]]; then
|
||||
echo "✓ Unpause schedule works"
|
||||
else
|
||||
echo "✗ Unpause schedule failed: $UNPAUSE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test delete schedule
|
||||
echo "Testing delete schedule..."
|
||||
DELETE_RESPONSE=$(curl -s -X POST http://localhost:8080/jobs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action": "delete", "job_id": "test-job"}')
|
||||
|
||||
if [[ $DELETE_RESPONSE == *"\"success\":true"* ]]; then
|
||||
echo "✓ Delete schedule works"
|
||||
else
|
||||
echo "✗ Delete schedule failed: $DELETE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -f $TEST_RECIPE
|
||||
|
||||
echo ""
|
||||
echo "🎉 All tests passed!"
|
||||
echo ""
|
||||
echo "The Temporal service is working correctly."
|
||||
echo "You can now integrate it with the Rust scheduler."
|
||||
Reference in New Issue
Block a user