/cronjob

Cron but with golang time specification. ⏰

Primary LanguageGoMIT LicenseMIT

CronJob build Go Report Card

CronJob is like cron but uses golang time specification.

go get it:

go get github.com/Lambels/cronjob@latest

Code Examples

Scheduling functions.

Its very simple, the cronjob object has AddFunc method exposed, you use this to add your function to the scheduler. AddFunc takes 3 parameters respectively: FuncJob, Schedule, ...JobConf. You can schedule functions before or after starting the processing thread.

FuncJob:

Job1 and Job2 implement the type FuncJob

func Job1() error {
    fmt.Println("Hello World, im a FuncJob")
    return nil
}

func Job2() error {
    fmt.Println("Hello World, im also a FuncJob")
    return fmt.Errorf("ERR")
}

Schedules:

Schedules determine the time at which your FunJob runs at.

package main

func main() {
    // runs in 5 seconds
    sched1 := cronjob.In(time.Now(), 5 * time.Second)

    // runs in 1 second
    sched2 := cronjob.In(time.Now(), 4 * time.Microsecond)

    // runs on 2022, 03, 16 at 16:18:59
    sched3 := cronjob.At(time.Date(
        2022,
        time.March,
        26,
        16,
        18,
        59,
        0,
        time.Local, // use cronjob location.
    ))

    // runs every hour.
    sched4 := cronjob.Every(1 * time.Hour)

    // runs on each 3 hour intervals: 03:00, 06:00, 09:00, 12:00, 15:00, 18:00, 21:00, 24:00
    sched5 := cronjob.EveryFixed(3 * time.Hour)
}

JobConf:

Job Configurations configure the behaviour of the job. Examples of such functions are found here.

A job configuration is a function with the signature JobConf func(*Job).

func Job1() error {
    fmt.Println("Hello World, im a FuncJob")
    return nil
}

func Job2() error {
    fmt.Println("Im a FuncJob which returns an error")
    return fmt.Errorf("ERR")
}

func main() {
    cron := cronjob.New()

    cron.AddFunc(
        Job1,
        cronjob.In(cron.Now(), 5 * time.Second),
        

        // configs:
        cronjob.WithRunOnStart(), // runs job on start.
    )

    cron.AddFunc(
        Job2,
        cronjob.In(cron.Now(), 10 * time.Second),

        // configs:
        cronjob.WithChain(
            // retry Job2 5 times in 5 second intervals.
            // always add cornjob.Retry() the first in the chain.
            cronjob.NewChain(cronjob.Retry(5 * time.Second, 5)),
        ),
    )
}

All together:

func Job1() error {
    fmt.Println("Hello World, im a FuncJob")
    return nil
}

func Job2() error {
    fmt.Println("Hello World, im also a FuncJob")
    return fmt.Errorf("ERR")
}

func main() {
    cron := cronjob.New()

    cron.AddFunc(Job1, cronjob.In(cron.Now(), 5 * time.Second), cronjob.WithRunOnStart())

    cron.Start()

    cron.AddFunc(Job2, cronjob.EveryFixed(cron.Now(), 1 * time.Hour))

    time.Sleep(1 * time.Hour)

    cron.Close()
}

Chains:

Chains allow you to customize the behavour of the Jobs when the jobs are running. A chain function making up a chain has the following signature: func(FuncJob) FuncJob. A chain is just a slice of these functions: type Chain []func(FuncJob) FuncJob.

cronjob.Retry() should always be added first in the chain to keep expected behaviour.

Inspiration from cron

Creating A Chain:

func SomeChain(fj cronjob.FuncJob) cronjob.FuncJob {
	return func() error {
		log.Println("Hello from SomeChain")
        return fj() // call next function in chain.
	}
}

func SomeOtherChain(fj cronjob.FuncJob) cronjob.FuncJob {
	return func() error {
		log.Println("Hello from SomeOtherChain")
        return fj() // call next function in chain.
	}
}

func Job() error {
    log.Println("Hello from Job")
    return nil
}

func main() {
    chain := cronjob.NewChain(SomeChain, SomeOtherChain)
    chain.Run(job)

    // output:
    // "Hello from SomeChain"
    // "Hello from SomeOtherChain"
    // "Hello from Job"
}

Merging n Chains:

func SomeChain(fj cronjob.FuncJob) cronjob.FuncJob {
	return func() error {
		log.Println("Hello from SomeChain")
        return fj() // call next function in chain.
	}
}

func SomeOtherChain(fj cronjob.FuncJob) cronjob.FuncJob {
	return func() error {
		log.Println("Hello from SomeOtherChain")
        return fj() // call next function in chain.
	}
}

func SomeOtherOtherChain(fj cronjob.FuncJob) cronjob.FuncJob {
	return func() error {
		log.Println("Hello from SomeOtherOtherChain")
        return fj() // call next function in chain.
	}
}

func Job() error {
    log.Println("Hello from Job")
    return nil
}

func main() {
    chain1 := cronjob.NewChain(SomeChain, SomeOtherChain)
    chain2 := cronjob.NewChain(SomeOtherOtherChain)

    chain3 := cronjob.MergeChains(chain1, chain2)
    chain3.Run(Job)

    // output:
    // "Hello from SomeChain"
    // "Hello from SomeOtherChain"
    // "Hello from SomeOtherOtherChain"
    // "Hello from Job"
}

Fault Tolerance:

always add cronjob.Retry() first in the chain!

func SomeChain(fj cronjob.FuncJob) cronjob.FuncJob {
	return func() error {
		log.Println("Hello from SomeChain")
        return fj() // call next function in chain.
	}
}

func Job() error {
    log.Println("Hello from Job")
    return fmt.Errorf("ERR")
}

func main() {
    chain1 := cronjob.NewChain(cronjob.Retry(5*time.Second, 5), SomeChain)

    chain1.Run(Job)

    // output:
    // "Hello from SomeChain"
    // "Hello from Job"
    // "Hello from SomeChain"
    // "Hello from Job"
    // "Hello from SomeChain"
    // "Hello from Job"
    // "Hello from SomeChain"
    // "Hello from Job"
    // "Hello from SomeChain"
    // "Hello from Job"
}

Removing Jobs:

The cronjob object has RemoveJob method exposed, it takes the job id as a parameter. RemoveJob will no-op if no job matches the id. You can call RemoveJob either after starting the processing thread or before.

Get Job ID:

func Job1() error {
    fmt.Println("Hello World, im a FuncJob")
    return nil
}

func main() {
    cron := cronjob.New()

    id := cron.AddFunc(Job1, cronjob.In(cron.Now(), 5 * time.Second))

    // store id ...
}

Remove Job:

func Job1() error {
    fmt.Println("Hello World, im a FuncJob")
    return nil
}

func main() {
    cron := cronjob.New()

    id := cron.AddFunc(Job1, cronjob.In(cron.Now(), 5 * time.Second))

    cron.RemoveJob(id)

    id := cron.AddFunc(Job1, cronjob.In(cron.Now(), 5 * time.Second))

    cron.Start()
    defer cron.Stop()
    
    cron.RemoveJob(id)
}

Stopping:

There are 2 ways to stop a cronjob's processing thread, Stop and StopWithFlush. Stop exits the processing thread and StopWithFlush exits the processing thread and runs the remaining jobs providing a context to wait for their completion.

Stop:

func Job1() error {
    fmt.Println("Hello World, im a FuncJob")
    return nil
}

func main() {
    cron := cronjob.New()

    cron.AddFunc(Job1, cronjob.In(cron.Now(), 5 * time.Second))

    cron.Start()
    cron.Stop()

    cron.AddFunc(Job1, cronjob.In(cron.Now(), 2 * time.Second)) // still works.
}

StopWithFlush:

func Job1() error {
    time.Sleep(1 * time.Hour)
    return nil
}
func Job2() error {
    time.Sleep(5 * time.Second)
    return nil
}

func main() {
    cron := cronjob.New()

    cron.AddFunc(Job1, cronjob.In(cron.Now(), 5 * time.Second))
    cron.AddFunc(Job1, cronjob.In(cron.Now(), 5 * time.Second))

    cron.Start()
    ctx := cron.StopWithFlush()
    <-ctx.Done() // waits for Job1 and Job2 to finish. (1 hour)

    cron.AddFunc(Job1, cronjob.In(cron.Now(), 2 * time.Second)) // still works.
}