CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/golang-github-com-go-co-op-gocron-v2

A Golang job scheduling library that lets you run Go functions at pre-determined intervals using cron expressions, fixed durations, daily, weekly, monthly, or one-time schedules with support for distributed deployments.

Overview
Eval results
Files

initialization.mddocs/examples/by-scenario/

Initialization Task Examples

Warmup tasks, one-time setup, migration jobs, and application startup tasks.

Application Warmup

Cache Warmup on Startup

// Warm up caches immediately on application start
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
    gocron.NewTask(func() error {
        log.Println("Warming up caches...")

        // Warm up user cache
        users := fetchActiveUsers()
        for _, user := range users {
            cache.Set(fmt.Sprintf("user:%d", user.ID), user, 1*time.Hour)
        }

        // Warm up product cache
        products := fetchPopularProducts()
        for _, product := range products {
            cache.Set(fmt.Sprintf("product:%d", product.ID), product, 30*time.Minute)
        }

        // Warm up configuration cache
        config := fetchConfiguration()
        cache.Set("app:config", config, 10*time.Minute)

        log.Println("Cache warmup complete")
        return nil
    }),
    gocron.WithName("cache-warmup"),
)

Database Connection Pool Warmup

// Initialize database connections on startup
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
    gocron.NewTask(func() error {
        log.Println("Warming up database connection pool...")

        // Pre-create connections
        for i := 0; i < 10; i++ {
            conn := db.GetConnection()
            // Simple query to test connection
            conn.Ping()
            db.ReleaseConnection(conn)
        }

        log.Println("Database connection pool ready")
        return nil
    }),
    gocron.WithName("db-pool-warmup"),
)

Service Discovery Registration

// Register with service discovery immediately
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
    gocron.NewTask(func() error {
        log.Println("Registering with service discovery...")

        serviceInfo := ServiceInfo{
            Name:    "my-service",
            Address: os.Getenv("SERVICE_ADDR"),
            Port:    os.Getenv("SERVICE_PORT"),
            Health:  "/health",
        }

        err := consul.RegisterService(serviceInfo)
        if err != nil {
            return fmt.Errorf("service registration failed: %w", err)
        }

        log.Println("Service registered successfully")
        return nil
    }),
    gocron.WithName("service-registration"),
)

Database Migrations

Run Migrations on Startup

// Run database migrations once on startup
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
    gocron.NewTask(func() error {
        log.Println("Running database migrations...")

        migrator := NewMigrator(db)

        // Check current version
        currentVersion := migrator.GetCurrentVersion()
        log.Printf("Current schema version: %d", currentVersion)

        // Run pending migrations
        err := migrator.MigrateUp()
        if err != nil {
            return fmt.Errorf("migration failed: %w", err)
        }

        newVersion := migrator.GetCurrentVersion()
        log.Printf("Migrated to version: %d", newVersion)

        return nil
    }),
    gocron.WithName("db-migration"),
    gocron.WithEventListeners(
        gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
            log.Printf("CRITICAL: Database migration failed: %v", err)
            // Don't start other jobs if migrations fail
            os.Exit(1)
        }),
    ),
)

Data Migration

// Migrate data format (run once)
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
    gocron.NewTask(func() error {
        // Check if migration already completed
        if migrationCompleted("data_format_v2") {
            log.Println("Data migration already completed")
            return nil
        }

        log.Println("Starting data format migration...")

        // Migrate in batches
        batchSize := 1000
        offset := 0
        totalMigrated := 0

        for {
            records := db.Query(`
                SELECT * FROM records
                WHERE format_version = 1
                LIMIT ? OFFSET ?
            `, batchSize, offset)

            if len(records) == 0 {
                break
            }

            for _, record := range records {
                migratedData := migrateDataFormat(record)
                db.Update("records", record.ID, migratedData)
            }

            totalMigrated += len(records)
            offset += batchSize

            log.Printf("Migrated %d/%d records", totalMigrated, offset)
        }

        // Mark migration as complete
        markMigrationComplete("data_format_v2")

        log.Printf("Data migration complete: %d records migrated", totalMigrated)
        return nil
    }),
    gocron.WithName("data-migration"),
)

Configuration Loading

Load Remote Configuration

// Load configuration from remote source on startup
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
    gocron.NewTask(func() error {
        log.Println("Loading remote configuration...")

        // Fetch configuration from config server
        config, err := configClient.FetchConfiguration("production")
        if err != nil {
            // Try fallback
            log.Printf("Remote config failed, using fallback: %v", err)
            config = loadLocalConfiguration()
        }

        // Apply configuration
        app.SetConfiguration(config)

        // Cache configuration
        cache.Set("app:config", config, 5*time.Minute)

        log.Println("Configuration loaded successfully")
        return nil
    }),
    gocron.WithName("config-load"),
)

Feature Flag Initialization

// Initialize feature flags on startup
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
    gocron.NewTask(func() error {
        log.Println("Initializing feature flags...")

        // Load feature flags
        flags, err := featureFlagService.GetAllFlags()
        if err != nil {
            return fmt.Errorf("failed to load feature flags: %w", err)
        }

        // Initialize flag cache
        for _, flag := range flags {
            featureFlags.Set(flag.Name, flag.Enabled)
        }

        log.Printf("Loaded %d feature flags", len(flags))
        return nil
    }),
    gocron.WithName("feature-flags-init"),
)

Scheduled Initialization

Delayed Service Start

// Wait for dependencies before starting service
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(time.Now().Add(30*time.Second))),
    gocron.NewTask(func() error {
        log.Println("Starting delayed service initialization...")

        // Wait for dependencies
        if !waitForDependency("database", 30*time.Second) {
            return errors.New("database not ready")
        }

        if !waitForDependency("cache", 30*time.Second) {
            return errors.New("cache not ready")
        }

        // Start service
        startService()

        log.Println("Service started successfully")
        return nil
    }),
    gocron.WithName("delayed-service-start"),
)

Staged Initialization

// Initialize components in stages
func setupStagedInitialization(s gocron.Scheduler) {
    now := time.Now()

    // Stage 1: Core components (immediate)
    s.NewJob(
        gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
        gocron.NewTask(func() error {
            log.Println("Stage 1: Initializing core components")
            initializeDatabase()
            initializeCache()
            return nil
        }),
        gocron.WithName("init-stage-1"),
    )

    // Stage 2: Application layer (after 10 seconds)
    s.NewJob(
        gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(now.Add(10*time.Second))),
        gocron.NewTask(func() error {
            log.Println("Stage 2: Initializing application layer")
            loadConfiguration()
            initializeServices()
            return nil
        }),
        gocron.WithName("init-stage-2"),
    )

    // Stage 3: Background workers (after 20 seconds)
    s.NewJob(
        gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(now.Add(20*time.Second))),
        gocron.NewTask(func() error {
            log.Println("Stage 3: Starting background workers")
            startWorkers()
            return nil
        }),
        gocron.WithName("init-stage-3"),
    )

    // Stage 4: Warmup (after 30 seconds)
    s.NewJob(
        gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(now.Add(30*time.Second))),
        gocron.NewTask(func() error {
            log.Println("Stage 4: Warming up caches")
            warmupCaches()
            return nil
        }),
        gocron.WithName("init-stage-4"),
    )
}

Health Check Initialization

Initial Health Check

// Perform initial health check before accepting traffic
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
    gocron.NewTask(func() error {
        log.Println("Performing initial health check...")

        checks := []HealthCheck{
            {Name: "database", Check: checkDatabase},
            {Name: "cache", Check: checkCache},
            {Name: "external-api", Check: checkExternalAPI},
            {Name: "disk-space", Check: checkDiskSpace},
        }

        failed := []string{}

        for _, check := range checks {
            err := check.Check()
            if err != nil {
                log.Printf("Health check failed: %s - %v", check.Name, err)
                failed = append(failed, check.Name)
            } else {
                log.Printf("Health check passed: %s", check.Name)
            }
        }

        if len(failed) > 0 {
            return fmt.Errorf("health checks failed: %v", failed)
        }

        log.Println("All health checks passed")
        return nil
    }),
    gocron.WithName("initial-health-check"),
)

Wait for Readiness

// Wait for all systems to be ready before marking service as ready
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
    gocron.NewTask(func() error {
        log.Println("Waiting for readiness...")

        timeout := time.After(60 * time.Second)
        ticker := time.NewTicker(2 * time.Second)
        defer ticker.Stop()

        for {
            select {
            case <-timeout:
                return errors.New("readiness timeout exceeded")
            case <-ticker.C:
                if isSystemReady() {
                    log.Println("System is ready")
                    markServiceReady()
                    return nil
                }
                log.Println("System not ready yet, waiting...")
            }
        }
    }),
    gocron.WithName("wait-readiness"),
)

Seed Data

Load Seed Data

// Load seed data for development/testing
j, _ := s.NewJob(
    gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
    gocron.NewTask(func() error {
        // Only run in development
        if os.Getenv("ENV") != "development" {
            log.Println("Skipping seed data (not in development)")
            return nil
        }

        // Check if already seeded
        if isDataSeeded() {
            log.Println("Database already seeded")
            return nil
        }

        log.Println("Loading seed data...")

        // Create test users
        users := createTestUsers(10)
        for _, user := range users {
            db.Insert("users", user)
        }

        // Create test products
        products := createTestProducts(50)
        for _, product := range products {
            db.Insert("products", product)
        }

        // Mark as seeded
        markDataSeeded()

        log.Println("Seed data loaded successfully")
        return nil
    }),
    gocron.WithName("seed-data"),
)

Complete Initialization Example

package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/go-co-op/gocron/v2"
)

func main() {
    s, _ := gocron.NewScheduler()
    defer s.Shutdown()

    // Step 1: Run migrations (blocking)
    s.NewJob(
        gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
        gocron.NewTask(func() error {
            log.Println("Running database migrations...")
            return runMigrations()
        }),
        gocron.WithName("db-migration"),
        gocron.WithEventListeners(
            gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
                log.Fatalf("Migration failed: %v", err)
            }),
        ),
    )

    // Step 2: Load configuration
    s.NewJob(
        gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(time.Now().Add(2*time.Second))),
        gocron.NewTask(func() error {
            log.Println("Loading configuration...")
            return loadConfiguration()
        }),
        gocron.WithName("config-load"),
    )

    // Step 3: Warm up caches
    s.NewJob(
        gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(time.Now().Add(5*time.Second))),
        gocron.NewTask(func() error {
            log.Println("Warming up caches...")
            return warmupCaches()
        }),
        gocron.WithName("cache-warmup"),
    )

    // Step 4: Health check
    s.NewJob(
        gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(time.Now().Add(10*time.Second))),
        gocron.NewTask(func() error {
            log.Println("Running health checks...")
            return performHealthChecks()
        }),
        gocron.WithName("health-check"),
    )

    // Step 5: Mark service ready
    s.NewJob(
        gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(time.Now().Add(15*time.Second))),
        gocron.NewTask(func() error {
            log.Println("Service is ready")
            markServiceReady()
            return nil
        }),
        gocron.WithName("mark-ready"),
    )

    // Start scheduler
    s.Start()
    log.Println("Initialization scheduler started")

    // Monitor initialization progress
    go func() {
        ticker := time.NewTicker(5 * time.Second)
        defer ticker.Stop()

        for range ticker.C {
            remaining := len(s.Jobs())
            if remaining == 0 {
                log.Println("All initialization jobs complete")
                return
            }
            log.Printf("%d initialization jobs remaining", remaining)
        }
    }()

    // Wait for interrupt
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
    <-sigCh

    log.Println("Shutting down...")
}

Best Practices

  1. Run critical initializations first (migrations, config loading)
  2. Use staged initialization for complex applications
  3. Implement health checks before accepting traffic
  4. Handle initialization failures appropriately (retry or exit)
  5. Make initializations idempotent (safe to run multiple times)
  6. Log initialization progress for debugging
  7. Use timeouts to prevent hanging on startup
  8. Warm up critical resources (caches, connection pools)

Related Documentation

  • Examples: One-Time Jobs
  • Examples: Web App Tasks
  • Guide: Job Lifecycle
  • API: Job Options

Install with Tessl CLI

npx tessl i tessl/golang-github-com-go-co-op-gocron-v2

docs

index.md

tile.json