cronx

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 7, 2022 License: BSD-3-Clause Imports: 12 Imported by: 0

README

Cronx

Cronx is a wrapper for robfig/cron. It includes a live monitoring of current schedule and state of active jobs that can be outputted as JSON or HTML template.

Available Status

  • Down => Job fails to be registered.
  • Up => Job has just been created.
  • Running => Job is currently running.
  • Idle => Job is waiting for next execution time.
  • Error => Job fails on the last run.

Quick Start

Create a main.go file.

package main

import (
	"context"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/rizalgowandy/gdk/pkg/cronx"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

// In order to create a job you need to create a struct that has Run() method.
type sendEmail struct{}

func (s sendEmail) Run(ctx context.Context) error {
	log.WithLevel(zerolog.InfoLevel).
		Str("job", "sendEmail").
		Msg("every 5 sec send reminder emails")
	return nil
}

func main() {
	// Create a cron controller with default config that:
	// - running on port :8998
	// - location is time.Local
	// - without any middleware
	cronx.Default()

	// Register a new cron job.
	// Struct name will become the name for the current job.
	if err := cronx.Schedule("@every 5s", sendEmail{}); err != nil {
		// create log and send alert we fail to register job.
		log.WithLevel(zerolog.ErrorLevel).
			Err(err).
			Msg("register sendEmail must success")
	}

    // Start HTTP server.
    cronx.Serve()
}

Get dependencies

$ go mod vendor -v

Start server

$ go run main.go

Browse to

{
  "data": [
    {
      "id": 1,
      "job": {
        "name": "sendEmail",
        "status": "RUNNING",
        "latency": "3.000299794s",
        "error": ""
      },
      "next_run": "2020-12-11T22:36:35+07:00",
      "prev_run": "2020-12-11T22:36:30+07:00"
    }
  ]
}

Custom Configuration

// Create a cron with custom config.
cronx.New(cronx.Config{
    Address:  ":8998", // Determine the built-in HTTP server port.
    Location: func() *time.Location { // Change timezone to Jakarta.
        jakarta, err := time.LoadLocation("Asia/Jakarta")
        if err != nil {
            secondsEastOfUTC := int((7 * time.Hour).Seconds())
            jakarta = time.FixedZone("WIB", secondsEastOfUTC)
        }
        return jakarta
    }(),
})

Interceptor / Middleware

Interceptor or commonly known as middleware is an operation that commonly executed before any of other operation. This library has the capability to add multiple middlewares that will be executed before or after the real job. It means you can log the running job, send telemetry, or protect the application from going down because of panic by adding middlewares. The idea of a middleware is to be declared once, and be executed on all registered jobs. Hence, reduce the code duplication on each job implementation.

Adding Interceptor / Middleware
// Create cron middleware.
// The order is important.
// The first one will be executed first.
middleware := cronx.Chain(
    interceptor.RequestID, // Inject request id to context.
    interceptor.Recover(), // Auto recover from panic.
    interceptor.Logger(), // Log start and finish process.
    interceptor.DefaultWorkerPool(), // Limit concurrent running job.
)

cronx.Default(middleware)

Check all available interceptors here.

Custom Interceptor / Middleware
// Sleep is a middleware that sleep a few second after job has been executed.
func Sleep() cronx.Interceptor {
	return func(ctx context.Context, job *cronx.Job, handler cronx.Handler) error {
		err := handler(ctx, job)
		time.Sleep(10 * time.Second)
		return err
	}
}

For more example check here.

Schedule Specification Format

Schedule
Field name Mandatory? Allowed values Allowed special characters
Seconds Optional 0-59 * / , -
Minutes Yes 0-59 * / , -
Hours Yes 0-23 * / , -
Day of month Yes 1-31 * / , - ?
Month Yes 1-12 or JAN-DEC * / , -
Day of week Yes 0-6 or SUN-SAT * / , - ?
Predefined schedules
Entry Description Equivalent
@yearly (or @annually) Run once a year, midnight, Jan. 1st 0 0 0 1 1 *
@monthly Run once a month, midnight, first of month 0 0 0 1 * *
@weekly Run once a week, midnight between Sat/Sun 0 0 0 * * 0
@daily (or @midnight) Run once a day, midnight 0 0 0 * * *
@hourly Run once an hour, beginning of hour 0 0 * * * *
Intervals
@every <duration>

For example, "@every 1h30m10s" would indicate a schedule that activates after 1 hour, 30 minutes, 10 seconds, and then every interval after that.

Please refer to this link for more detail.

FAQ

What are the available commands?

Here the list of commonly used commands.

// Schedule sets a job to run at specific time.
// Example:
//  @every 5m
//  0 */10 * * * * => every 10m
Schedule(spec string, job JobItf) error

// Schedules sets a job to run multiple times at specific time.
// Symbol */,-? should never be used as separator character.
// These symbols are reserved for cron specification.
//
// Example:
//  Spec		: "0 0 1 * * *#0 0 2 * * *#0 0 3 * * *
//  Separator	: "#"
//  This input schedules the job to run 3 times.
Schedules(spec, separator string, job JobItf) error

// Every executes the given job at a fixed interval.
// The interval provided is the time between the job ending and the job being run again.
// The time that the job takes to run is not included in the interval.
// Minimal time is 1 sec.
Every(duration time.Duration, job JobItf)

Go to here to see the list of available commands.

What are the available interceptors?

Go to here to see the available interceptors.

Can I use my own router without starting the built-in router?

Yes, you can. This library is very modular.

// Since we want to create custom HTTP server.
// Do not forget to shutdown the cron gracefully manually here.
cronx.New()
defer cronx.Stop()

// GetStatusData will return the []cronx.StatusData.
// You can use this data like any other Golang data structure.
// You can print it, or even serves it using your own router.
res := cronx.GetStatusData() 

// An example using gin as the router.
r := gin.Default()
r.GET("/custom-path", func(c *gin.Context) {
    c.JSON(http.StatusOK, map[string]interface{}{
    	"data": res,
    })
})

// Start your own server and don't call cronx.Serve().
r.Run()

Here's another example.

Can I still get the built-in template if I use my own router?

Yes, you can.

// GetStatusTemplate will return the built-in status page template.
index, _ := page.GetStatusTemplate()

// An example using echo as the router.
e := echo.New()
index, _ := page.GetStatusTemplate()
e.GET("jobs", func(context echo.Context) error {
    // Serve the template to the writer and pass the current status data.
    return index.Execute(context.Response().Writer, cronx.GetStatusData())
})

Here's another example.

Server is located in the US, but my user is in Jakarta, can I change the cron timezone?

Yes, you can. By default, the cron timezone will follow the server location timezone using time.Local. If you placed the server in the US, it will use the US timezone. If you placed the server in the SG, it will use the SG timezone.

// Create a custom config.
cronx.New(cronx.Config{
    Address:  ":8998",
    Location: func() *time.Location { // Change timezone to Jakarta.
        jakarta, err := time.LoadLocation("Asia/Jakarta")
        if err != nil {
            secondsEastOfUTC := int((7 * time.Hour).Seconds())
            jakarta = time.FixedZone("WIB", secondsEastOfUTC)
        }
        return jakarta
    }(),
})
My job requires certain information like current wave number, how can I get this information?

This kind of information is stored inside metadata, which stored automatically inside context.

type subscription struct{}

func (subscription) Run(ctx context.Context) error {
	md, ok := cronx.GetJobMetadata(ctx)
	if !ok {
		return errors.New("cannot job metadata")
	}

	log.WithLevel(zerolog.InfoLevel).
		Str("job", "subscription").
		Interface("metadata", md).
		Msg("is running")
	return nil
}

Documentation

Index

Constants

View Source
const (
	// CtxKeyJobMetadata is context for cron job metadata.
	CtxKeyJobMetadata = contextKey("cron-job-metadata")
)

Context key for standardized context value.

View Source
const SleepDuration = time.Second * 10

SleepDuration defines the duration to sleep the server if the defined address is busy.

Variables

This section is empty.

Functions

func Custom added in v0.2.0

func Custom(config Config, interceptors ...Interceptor)

Custom creates a cron with custom config. For advance user, allow custom modification.

func Default

func Default(interceptors ...Interceptor)

Default creates a cron with default config. HTTP server is built in as side car by default.

func Every

func Every(duration time.Duration, job JobItf)

Every executes the given job at a fixed interval. The interval provided is the time between the job ending and the job being run again. The time that the job takes to run is not included in the interval. Minimal time is 1 sec.

func GetEntries

func GetEntries() []cron.Entry

GetEntries returns all the current registered jobs.

func GetEntry

func GetEntry(id cron.EntryID) *cron.Entry

GetEntry returns a snapshot of the given entry, or nil if it couldn't be found.

func GetInfo

func GetInfo() map[string]interface{}

GetInfo returns current cron check basic information.

func GetStatusJSON

func GetStatusJSON() map[string]interface{}

GetStatusJSON returns all jobs status as map[string]interface.

func New

func New(interceptors ...Interceptor)

New creates a cron without HTTP server built in.

func NewServer

func NewServer(address string) (*http.Server, error)

NewServer creates a new HTTP server. - / => current server status. - /jobs => current jobs as frontend html. - /api/jobs => current jobs as json.

func NewSideCarServer added in v0.2.0

func NewSideCarServer(commandCtrl *CommandController)

NewSideCarServer creates a new side car HTTP server. HTTP server will be start automatically. - / => current server status. - /jobs => current jobs as frontend html. - /api/jobs => current jobs as json.

func Remove

func Remove(id cron.EntryID)

Remove removes a specific job from running. Get EntryID from the list job entries cronx.GetEntries(). If job is in the middle of running, once the process is finished it will be removed.

func Schedule

func Schedule(spec string, job JobItf) error

Schedule sets a job to run at specific time. Example:

@every 5m
0 */10 * * * * => every 10m

func ScheduleWithName added in v0.2.0

func ScheduleWithName(name, spec string, job JobItf) error

ScheduleWithName sets a job to run at specific time with a Job name Example:

@every 5m
0 */10 * * * * => every 10m

func Schedules

func Schedules(spec, separator string, job JobItf) error

Schedules sets a job to run multiple times at specific time. Symbol */,-? should never be used as separator character. These symbols are reserved for cron specification.

Example:

Spec		: "0 0 1 * * *#0 0 2 * * *#0 0 3 * * *
Separator	: "#"
This input schedules the job to run 3 times.

func SetJobMetadata

func SetJobMetadata(ctx context.Context, meta JobMetadata) context.Context

SetJobMetadata stores current job metadata inside current context.

func Stop

func Stop()

Stop stops active jobs from running at the next scheduled time.

Types

type CommandController

type CommandController struct {
	// Commander holds all the underlying cron jobs.
	Commander *cron.Cron
	// Interceptor holds middleware that will be executed before current job operation.
	Interceptor Interceptor
	// Parser is a custom parser to support v1 that contains second as first parameter.
	Parser cron.Parser
	// UnregisteredJobs describes the list of jobs that have been failed to be registered.
	UnregisteredJobs []*Job
	// Address determines the address will we serve the json and frontend status.
	// Empty string meaning we won't serve the current job status.
	Address string
	// Location describes the timezone current cron is running.
	// By default the timezone will be the same timezone as the server.
	Location *time.Location
	// CreatedTime describes when the command controller created.
	CreatedTime time.Time
}

CommandController controls all the underlying job.

func NewCommandController

func NewCommandController(config Config, interceptors ...Interceptor) *CommandController

NewCommandController create a command controller with a specific config.

func (*CommandController) Info

func (c *CommandController) Info() map[string]interface{}

Info returns command controller basic information.

func (*CommandController) StatusData

func (c *CommandController) StatusData() []StatusData

StatusData returns all jobs status.

func (*CommandController) StatusJSON

func (c *CommandController) StatusJSON() map[string]interface{}

StatusJSON returns all jobs status as map[string]interface.

type Config

type Config struct {
	// Address determines the address will we serve the json and frontend status.
	// Empty string meaning we won't serve the current job status.
	Address string
	// Location describes the timezone current cron is running.
	Location *time.Location
}

Config defines the config for the command controller.

type Func

type Func func(ctx context.Context) error

Func is a type to allow callers to wrap a raw func. Example:

cronx.Schedule("@every 5m", cronx.Func(myFunc))

func (Func) Run

func (r Func) Run(ctx context.Context) error

type Handler

type Handler func(ctx context.Context, job *Job) error

Handler is the handler definition to run a job.

type Interceptor

type Interceptor func(ctx context.Context, job *Job, handler Handler) error

Interceptor is the middleware that will be executed before the current handler.

func Chain

func Chain(interceptors ...Interceptor) Interceptor

Chain returns a single interceptor from multiple interceptors.

type Job

type Job struct {
	JobMetadata

	Name    string     `json:"name"`
	Status  StatusCode `json:"status"`
	Latency string     `json:"latency"`
	Error   string     `json:"error"`
	// contains filtered or unexported fields
}

func NewJob

func NewJob(job JobItf, name string, waveNumber, totalWave int64) *Job

NewJob creates a new job with default status and name.

func (*Job) Run

func (j *Job) Run()

Run executes the current job operation.

func (*Job) UpdateStatus

func (j *Job) UpdateStatus() StatusCode

UpdateStatus updates the current job status to the latest.

type JobItf

type JobItf interface {
	Run(ctx context.Context) error
}

type JobMetadata

type JobMetadata struct {
	EntryID    cron.EntryID `json:"entry_id"`
	Wave       int64        `json:"wave"`
	TotalWave  int64        `json:"total_wave"`
	IsLastWave bool         `json:"is_last_wave"`
}

func GetJobMetadata

func GetJobMetadata(ctx context.Context) (JobMetadata, bool)

GetJobMetadata returns job metadata from current context, and status if it exists or not.

type ServerController

type ServerController struct {
	// CommandController controls all the underlying job.
	CommandController *CommandController
}

ServerController is http server controller.

func (*ServerController) APIJobs

func (c *ServerController) APIJobs(ctx echo.Context) error

APIJobs returns job status as json.

func (*ServerController) HealthCheck

func (c *ServerController) HealthCheck(ctx echo.Context) error

HealthCheck returns server status.

func (*ServerController) Jobs

func (c *ServerController) Jobs(ctx echo.Context) error

Jobs return job status as frontend template.

type StatusCode

type StatusCode string

StatusCode describes current job status.

const (
	// StatusCodeUp describes that current job has just been created.
	StatusCodeUp StatusCode = "UP"
	// StatusCodeIdle describes that current job is waiting for next execution time.
	StatusCodeIdle StatusCode = "IDLE"
	// StatusCodeRunning describes that current job is currently running.
	StatusCodeRunning StatusCode = "RUNNING"
	// StatusCodeDown describes that current job has failed to be registered.
	StatusCodeDown StatusCode = "DOWN"
	// StatusCodeError describes that last run has failed.
	StatusCodeError StatusCode = "ERROR"
)

type StatusData

type StatusData struct {
	// ID is unique per job.
	ID cron.EntryID `json:"id,omitempty"`
	// Job defines current job.
	Job *Job `json:"job,omitempty"`
	// Next defines the next schedule to execute current job.
	Next time.Time `json:"next_run,omitempty"`
	// Prev defines the last run of the current job.
	Prev time.Time `json:"prev_run,omitempty"`
}

StatusData defines current job status.

func GetStatusData

func GetStatusData() []StatusData

GetStatusData returns all jobs status.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL