sim icon indicating copy to clipboard operation
sim copied to clipboard

feat(scheduler): add internal scheduler for self-hosted environments

Open majiayu000 opened this issue 1 month ago • 2 comments

Summary

  • Add built-in internal scheduler for self-hosted environments
  • Scheduler polls /api/schedules/execute to trigger scheduled workflows
  • Enabled by default in docker-compose.prod.yml for self-hosted setups
  • Configurable via ENABLE_INTERNAL_SCHEDULER, CRON_SECRET, and INTERNAL_SCHEDULER_INTERVAL_MS environment variables

Test plan

  • [x] Added unit tests for internal scheduler
  • [x] All tests pass (4 tests)
  • [ ] Manual testing with Docker deployment

Fixes #1870

majiayu000 avatar Dec 22 '25 10:12 majiayu000

@majiayu000 is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

vercel[bot] avatar Dec 22 '25 10:12 vercel[bot]

Greptile Summary

This PR adds a built-in internal scheduler for self-hosted environments that periodically polls /api/schedules/execute to trigger scheduled workflows, solving the issue where schedule triggers don't work without external cron services like Vercel Cron.

Key Changes

  • Internal Scheduler Module: New internal-scheduler.ts implements polling logic with configurable interval (default 60s), proper error handling, and graceful shutdown
  • App Integration: Scheduler initializes during app startup via instrumentation-node.ts register hook
  • Configuration: Three new environment variables added: ENABLE_INTERNAL_SCHEDULER, CRON_SECRET, and INTERNAL_SCHEDULER_INTERVAL_MS
  • Docker Setup: Enabled by default in docker-compose.prod.yml with sensible defaults
  • Test Coverage: Comprehensive unit tests for scheduler functionality

Implementation Details

The scheduler uses a simple polling mechanism with safeguards against concurrent executions. It authenticates using the existing CRON_SECRET via Bearer token, integrating seamlessly with the existing /api/schedules/execute endpoint that already supports both Trigger.dev and direct execution modes.

Issues Found

  • Critical: docker-compose.prod.yml is missing required INTERNAL_API_SECRET environment variable, which will prevent the app from starting
  • Style: Environment variable types should use z.boolean() and z.number() instead of z.string() for consistency with codebase patterns
  • Minor: Signal handlers could accumulate if initializeInternalScheduler() is called multiple times (hot reload scenarios)

Confidence Score: 3/5

  • This PR has a critical configuration issue that will prevent Docker deployments from starting, but the core implementation is solid
  • The scheduler implementation itself is well-designed with proper error handling, tests, and graceful shutdown. However, the missing INTERNAL_API_SECRET in docker-compose.prod.yml is a critical issue that will cause immediate failures for users following the self-hosted Docker setup. The score reflects this deployment blocker rather than the quality of the scheduler code itself.
  • docker-compose.prod.yml requires the missing INTERNAL_API_SECRET environment variable to be added before merge

Important Files Changed

Filename Overview
apps/sim/lib/scheduler/internal-scheduler.ts Core scheduler implementation with proper error handling and graceful shutdown. Implementation is solid but has a potential race condition with multiple SIGTERM/SIGINT handlers.
apps/sim/lib/core/config/env.ts Added environment variables for scheduler configuration. Type should be z.boolean() instead of z.string() for consistency with other boolean flags.
docker-compose.prod.yml Added scheduler configuration but missing required INTERNAL_API_SECRET environment variable which is needed for the app to start.

Sequence Diagram

sequenceDiagram
    participant App as Sim Application
    participant Scheduler as Internal Scheduler
    participant API as /api/schedules/execute
    participant DB as Database
    participant Workflow as Workflow Execution

    Note over App: App Startup (instrumentation-node.ts)
    App->>Scheduler: initializeInternalScheduler()
    
    alt ENABLE_INTERNAL_SCHEDULER !== 'true'
        Scheduler-->>App: Skip initialization
    else CRON_SECRET not configured
        Scheduler-->>App: Log warning, skip
    else Configuration valid
        Scheduler->>Scheduler: startInternalScheduler()
        Note over Scheduler: Set interval timer (default: 60s)
        Scheduler->>Scheduler: pollSchedules() immediately
    end

    loop Every INTERNAL_SCHEDULER_INTERVAL_MS
        Scheduler->>Scheduler: Check if previous poll running
        alt Previous poll still running
            Scheduler->>Scheduler: Skip this cycle
        else Ready to poll
            Scheduler->>API: GET /api/schedules/execute<br/>Authorization: Bearer {CRON_SECRET}
            API->>API: verifyCronAuth()
            
            alt Auth fails
                API-->>Scheduler: 401 Unauthorized
                Scheduler->>Scheduler: Log error
            else Auth succeeds
                API->>DB: Query due schedules<br/>(nextRunAt <= now, status != disabled)
                DB-->>API: Return due schedules
                API->>DB: Update lastQueuedAt
                
                loop For each due schedule
                    alt Trigger.dev enabled
                        API->>Workflow: tasks.trigger('schedule-execution')
                    else Trigger.dev disabled
                        API->>Workflow: executeScheduleJob() directly
                    end
                end
                
                API-->>Scheduler: 200 OK {executedCount: N}
                Scheduler->>Scheduler: Log execution count
            end
        end
    end

    Note over App: Graceful Shutdown (SIGTERM/SIGINT)
    App->>Scheduler: Signal handler triggered
    Scheduler->>Scheduler: stopInternalScheduler()
    Scheduler->>Scheduler: clearInterval()

greptile-apps[bot] avatar Dec 22 '25 11:12 greptile-apps[bot]