Введение
cron
Библиотека для управления запланированными задачами, реализованная на Go в Linux.crontab
действие этой команды. Мы также представили аналогичную библиотеку Go раньше —gron
.gron
Код небольшой и его лучше изучить. Но его функция относительно проста, и он больше не поддерживается. Если есть необходимость в запланированных задачах, рекомендуется использоватьcron
.
быстрый в использовании
Текстовый код использует модули Go.
Создайте каталог и инициализируйте:
$ mkdir cron && cd cron
$ go mod init github.com/darjun/go-daily-lib/cron
Установитьcron
, последняя стабильная версия — v3:
$ go get -u github.com/robfig/cron/v3
использовать:
package main
import (
"fmt"
"time"
"github.com/robfig/cron/v3"
)
func main() {
c := cron.New()
c.AddFunc("@every 1s", func() {
fmt.Println("tick every 1 second")
})
c.Start()
time.Sleep(time.Second * 5)
}
Очень прост в использовании, создайтеcron
Object, этот объект используется для управления временными задачами.
перечислитьcron
объектAddFunc()
метод добавления запланированной задачи в менеджер.AddFunc()
Принимает два параметра, параметр 1 задает правило времени срабатывания в виде строки, а параметр 2 представляет собой функцию без параметров, которая вызывается при каждом срабатывании.@every 1s
означает, что он срабатывает один раз в секунду,@every
После него добавляется временной интервал, указывающий, как часто он срабатывает один раз. Например@every 1h
означает, что он срабатывает каждый час,@every 1m2s
Указывает, что он срабатывает каждые 1 минуту и 2 секунды.time.ParseDuration()
Здесь можно использовать все поддерживаемые форматы.
перечислитьc.Start()
Запустите временную петлю.
Будьте осторожны, потому чтоc.Start()
Запускаем новую горутину для обнаружения циклов, мы добавили строчку в конец кодаtime.Sleep(time.Second * 5)
Предотвратить выход основной горутины.
Запустите эффект, выведите строку строк каждые 1 с:
$ go run main.go
tick every 1 second
tick every 1 second
tick every 1 second
tick every 1 second
tick every 1 second
Формат времени
с Linuxcrontab
Команда похожа,cron
поддержка библиотеки5поля, разделенные пробелами, для представления времени. Значения этих пяти полей следующие:
-
Minutes
: минуты, диапазон значений[0-59]
, поддерживает специальные символы* / , -
; -
Hours
: час, диапазон значений[0-23]
, поддерживает специальные символы* / , -
; -
Day of month
: День месяца, диапазон значений[1-31]
, поддерживает специальные символы* / , - ?
; -
Month
: месяц, диапазон значений[1-12]
Или используйте сокращения месяцев[JAN-DEC]
, поддерживает специальные символы* / , -
; -
Day of week
: Недельный календарь, диапазон значений[0-6]
или инициалы[JUN-SAT]
, поддерживает специальные символы* / , - ?
.
Обратите внимание, что названия месяцев и недель календаря нечувствительны к регистру, т.е.SUN/Sun/sun
Означает то же самое (оба воскресенья).
Специальные символы имеют следующие значения:
-
*
:использовать*
может соответствовать любому значению, например, установить в поле месяца (4-й) значение*
, с указанием каждого месяца; -
/
: используется для указания диапазонаразмер шага, например, установите для поля часа (2nd) значение3-59/15
Указывает, что триггер срабатывает на 3-й минуте, а затем каждые 15 минут, поэтому второй триггер — это 18-я минута, а третий триггер — 33 минуты. . . пока минуты не превысят 59; -
,
: используется для перечисления некоторых дискретных значений и нескольких диапазонов, таких как установка домена (5-го) еженедельного календаря наMON,WED,FRI
означает понедельник, среду и пятницу; -
-
: используется для указания диапазона, например, установите для поля часа (1-й) значение9-17
означает с 9:00 до 17:00 (включая 9 и 17); -
?
: можно использовать только в полях месячного и недельного календарей вместо*
, который представляет любой день месяца/недели.
Зная правила, мы можем определить произвольное время:
-
30 * * * *
: Поле минут равно 30, все остальные поля*
значит произвольный. Запускается в 30 минут каждого часа; -
30 3-6,20-23 * * *
: 30 для минутного поля и 30 для часового поля3-6,20-23
Показывает от 3 до 6 часов и от 20 до 23 часов. 30-минутный триггер в 3,4,5,6,20,21,22,23; -
0 0 1 1 *
: 1 (4-й) Срабатывает в 0 (2-й) час и 0 (1-я) минута 1-го (3-го) числа месяца.
Как только вы запомните порядок этих полей, вам будет легко освоить формат после нескольких практических занятий. Как только вы ознакомитесь с правилами, вы сможете использовать их умелоcrontab
заказ.
func main() {
c := cron.New()
c.AddFunc("30 * * * *", func() {
fmt.Println("Every hour on the half hour")
})
c.AddFunc("30 3-6,20-23 * * *", func() {
fmt.Println("On the half hour of 3-6am, 8-11pm")
})
c.AddFunc("0 0 1 1 *", func() {
fmt.Println("Jun 1 every year")
})
c.Start()
for {
time.Sleep(time.Second)
}
}
Предопределенные правила времени
Для удобства использования,cron
Некоторые временные правила предопределены:
-
@yearly
: также можно написать@annually
, что соответствует 0:00 первого дня каждого года. Эквивалентно0 0 1 1 *
; -
@monthly
: Обозначает 0:00 первого дня каждого месяца. Эквивалентно0 0 1 * *
; -
@weekly
: представляет 0:00 первого дня недели.Обратите внимание, что первый день — воскресенье, то есть 0:00, которое заканчивается в субботу и начинается в воскресенье. Эквивалентно0 0 * * 0
; -
@daily
: также можно написать@midnight
, что означает 0:00 каждый день. Эквивалентно0 0 * * *
; -
@hourly
: Указывает начало каждого часа. Эквивалентно0 * * * *
.
Например:
func main() {
c := cron.New()
c.AddFunc("@hourly", func() {
fmt.Println("Every hour")
})
c.AddFunc("@daily", func() {
fmt.Println("Every day on midnight")
})
c.AddFunc("@weekly", func() {
fmt.Println("Every week")
})
c.Start()
for {
time.Sleep(time.Second)
}
}
Вышеприведенный код является просто демонстрацией использования, и вывод реальной операции может занять очень много времени.
фиксированный интервал времени
cron
Поддерживается фиксированный интервал времени, формат:
@every <duration>
означает каждыйduration
Запустить один раз.<duration>
позвонюtime.ParseDuration()
разбор функции, поэтомуParseDuration
Доступны все поддерживаемые форматы. Например1h30m10s
. В разделе быстрого старта мы продемонстрировали@every
использования и не будет повторяться здесь.
Часовой пояс
По умолчанию все время основано на текущем часовом поясе. Конечно, мы также можем указать часовой пояс, есть 2 способа:
- Предварительно добавьте строку времени с
CRON_TZ=
+ Конкретный часовой пояс, формат конкретного часового пояса передcarbon
подробно в статье. Часовой пояс ТокиоAsia/Tokyo
, часовой пояс Нью-ЙоркаAmerica/New_York
; - Создайте
cron
Добавить параметр часового пояса, когда объектcron.WithLocation(location)
,location
заtime.LoadLocation(zone)
загруженный объект часового пояса,zone
для определенного формата часового пояса. или позвоните в уже созданныйcron
объектSetLocation()
метод установки часового пояса.
Пример:
func main() {
nyc, _ := time.LoadLocation("America/New_York")
c := cron.New(cron.WithLocation(nyc))
c.AddFunc("0 6 * * ?", func() {
fmt.Println("Every 6 o'clock at New York")
})
c.AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", func() {
fmt.Println("Every 6 o'clock at Tokyo")
})
c.Start()
for {
time.Sleep(time.Second)
}
}
Job
интерфейс
В дополнение к прямому использованию функций без параметров в качестве обратных вызовов,cron
также поддерживаетJob
интерфейс:
// cron.go
type Job interface {
Run()
}
Определяем интерфейс реализацииJob
Структура:
type GreetingJob struct {
Name string
}
func (g GreetingJob) Run() {
fmt.Println("Hello ", g.Name)
}
перечислитьcron
объектAddJob()
метод будетGreetingJob
Объект добавляется в диспетчер времени:
func main() {
c := cron.New()
c.AddJob("@every 1s", GreetingJob{"dj"})
c.Start()
time.Sleep(5 * time.Second)
}
текущий результат:
$ go run main.go
Hello dj
Hello dj
Hello dj
Hello dj
Hello dj
Использование пользовательской структуры позволяет задачам переносить состояние (Name
поле).
ФактическиAddFunc()
Также вызывается внутри методаAddJob()
метод. первый,cron
на основеfunc()
Тип определяет новый типFuncJob
:
// cron.go
type FuncJob func()
тогда пустьFuncJob
выполнитьJob
интерфейс:
// cron.go
func (f FuncJob) Run() {
f()
}
существуетAddFunc()
метод, преобразуйте входящий обратный вызов вFuncJob
пиши, потом звониAddJob()
метод:
func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {
return c.AddJob(spec, FuncJob(cmd))
}
потокобезопасность
cron
Создается новая горутина для выполнения обратного вызова триггера. Если этим обратным вызовам требуется одновременный доступ к некоторым ресурсам и данным, нам нужно выполнить синхронизацию явно.
пользовательский формат времени
cron
Поддержка гибкого формата времени. Если формат по умолчанию не соответствует требованиям, мы можем сами определить формат времени. требуется строка правила времениcron.Parser
объект для разбора. Давайте сначала посмотрим, как работает парсер по умолчанию.
Сначала определите каждый домен:
// parser.go
const (
Second ParseOption = 1 << iota
SecondOptional
Minute
Hour
Dom
Month
Dow
DowOptional
Descriptor
)
КромеMinute/Hour/Dom(Day of month)/Month/Dow(Day of week)
Кроме того, он также может поддерживатьSecond
. Относительный порядок фиксирован:
// parser.go
var places = []ParseOption{
Second,
Minute,
Hour,
Dom,
Month,
Dow,
}
var defaults = []string{
"0",
"0",
"0",
"*",
"*",
"*",
}
Формат времени по умолчанию использует 5 полей.
мы можем позвонитьcron.NewParser()
создай свой собственныйParser
объект, передавая в котором поля используются в битовом формате, например следующиеParser
Использовать 6 доменов, поддержкаSecond
(Второй):
parser := cron.NewParser(
cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
)
перечислитьcron.WithParser(parser)
Создайте параметр для передачи в конструктореcron.New()
, вы можете указать секунды при использовании:
c := cron.New(cron.WithParser(parser))
c.AddFunc("1 * * * * *", func () {
fmt.Println("every 1 second")
})
c.Start()
Этот формат времени должен использовать 6 полей в том же порядке, что и выше.const
Определения совпадают.
Поскольку приведенный выше формат времени слишком распространен,cron
Функция удобства определяется:
// option.go
func WithSeconds() Option {
return WithParser(NewParser(
Second | Minute | Hour | Dom | Month | Dow | Descriptor,
))
}
УведомлениеDescriptor
выражать право@every/@hour
и т.п. поддержка. имеютWithSeconds()
, нам не нужно вручную создаватьParser
объект:
c := cron.New(cron.WithSeconds())
Опции
cron
Создание объекта использует шаблон параметров, мы уже представили 3 варианта:
-
WithLocation
: указать часовой пояс; -
WithParser
: использовать собственный парсер; -
WithSeconds
: Пусть формат времени поддерживает секунды, которые на самом деле вызываются внутреннеWithParser
.
cron
Также доступны два других варианта:
-
WithLogger
: настроитьLogger
; -
WithChain
: Оболочка задания.
WithLogger
WithLogger
можно установитьcron
Внутреннее использование нашего обычаяLogger
:
func main() {
c := cron.New(
cron.WithLogger(
cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))))
c.AddFunc("@every 1s", func() {
fmt.Println("hello world")
})
c.Start()
time.Sleep(5 * time.Second)
}
Звоните вышеcron.VerbosPrintfLogger()
Упаковкаlog.Logger
,этоlogger
будет подробно записаноcron
Процесс внутреннего планирования:
$ go run main.go
cron: 2020/06/26 07:09:14 start
cron: 2020/06/26 07:09:14 schedule, now=2020-06-26T07:09:14+08:00, entry=1, next=2020-06-26T07:09:15+08:00
cron: 2020/06/26 07:09:15 wake, now=2020-06-26T07:09:15+08:00
cron: 2020/06/26 07:09:15 run, now=2020-06-26T07:09:15+08:00, entry=1, next=2020-06-26T07:09:16+08:00
hello world
cron: 2020/06/26 07:09:16 wake, now=2020-06-26T07:09:16+08:00
cron: 2020/06/26 07:09:16 run, now=2020-06-26T07:09:16+08:00, entry=1, next=2020-06-26T07:09:17+08:00
hello world
cron: 2020/06/26 07:09:17 wake, now=2020-06-26T07:09:17+08:00
cron: 2020/06/26 07:09:17 run, now=2020-06-26T07:09:17+08:00, entry=1, next=2020-06-26T07:09:18+08:00
hello world
cron: 2020/06/26 07:09:18 wake, now=2020-06-26T07:09:18+08:00
hello world
cron: 2020/06/26 07:09:18 run, now=2020-06-26T07:09:18+08:00, entry=1, next=2020-06-26T07:09:19+08:00
cron: 2020/06/26 07:09:19 wake, now=2020-06-26T07:09:19+08:00
hello world
cron: 2020/06/26 07:09:19 run, now=2020-06-26T07:09:19+08:00, entry=1, next=2020-06-26T07:09:20+08:0
Смотрим по умолчаниюLogger
как это выглядит:
// logger.go
var DefaultLogger Logger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))
func PrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {
return printfLogger{l, false}
}
func VerbosePrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {
return printfLogger{l, true}
}
type printfLogger struct {
logger interface{ Printf(string, ...interface{}) }
logInfo bool
}
WithChain
Оболочка задания может выполнить фактическийJob
Добавьте немного логики до и после:
- захватывать
panic
; - если
Job
Последний запуск не закончился, отложите это выполнение; - если
Job
Последний запуск не был введен, пропустите это выполнение; - записывать каждый
Job
выполнение.
мы можем поставитьChain
Аналогия — промежуточное ПО для веб-процессоров. по фактуJob
Логика выполнения внешне инкапсулирует слой логики. Наша логика инкапсуляции должна быть записана как функция,Job
тип, возвращает инкапсулированныйJob
.cron
определяет тип для такой функцииJobWrapper
:
// chain.go
type JobWrapper func(Job) Job
затем используйтеChain
будут ли этиJobWrapper
Соединить:
type Chain struct {
wrappers []JobWrapper
}
func NewChain(c ...JobWrapper) Chain {
return Chain{c}
}
перечислитьChain
объектThen(job)
метод применения этихJobWrapper
, который возвращает окончательное задание `Job:
func (c Chain) Then(j Job) Job {
for i := range c.wrappers {
j = c.wrappers[len(c.wrappers)-i-1](j)
}
return j
}
Обратите внимание на приложениеJobWrapper
порядок.
встроенныйJobWrapper
cron
Встроено еще 3 б/уJobWrapper
:
-
Recover
: захват внутриJob
Генерируемая паника; -
DelayIfStillRunning
: при срабатывании, если последняя задача не была завершена (это занимает слишком много времени), она будет ждать завершения последней задачи перед ее выполнением; -
SkipIfStillRunning
: при срабатывании, если последняя задача не была завершена, это выполнение будет пропущено.
Они представлены отдельно ниже.
Recover
Давайте сначала посмотрим, как его использовать:
type panicJob struct {
count int
}
func (p *panicJob) Run() {
p.count++
if p.count == 1 {
panic("oooooooooooooops!!!")
}
fmt.Println("hello world")
}
func main() {
c := cron.New()
c.AddJob("@every 1s", cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(&panicJob{}))
c.Start()
time.Sleep(5 * time.Second)
}
panicJob
На первом триггере триггерpanic
. потому что этоcron.Recover()
защиты, а также могут выполняться последующие задачи:
go run main.go
cron: 2020/06/27 14:02:00 panic, error=oooooooooooooops!!!, stack=...
goroutine 18 [running]:
github.com/robfig/cron/v3.Recover.func1.1.1(0x514ee0, 0xc0000044a0)
D:/code/golang/pkg/mod/github.com/robfig/cron/v3@v3.0.1/chain.go:45 +0xbc
panic(0x4cf380, 0x513280)
C:/Go/src/runtime/panic.go:969 +0x174
main.(*panicJob).Run(0xc0000140e8)
D:/code/golang/src/github.com/darjun/go-daily-lib/cron/recover/main.go:17 +0xba
github.com/robfig/cron/v3.Recover.func1.1()
D:/code/golang/pkg/mod/github.com/robfig/cron/v3@v3.0.1/chain.go:53 +0x6f
github.com/robfig/cron/v3.FuncJob.Run(0xc000070390)
D:/code/golang/pkg/mod/github.com/robfig/cron/v3@v3.0.1/cron.go:136 +0x2c
github.com/robfig/cron/v3.(*Cron).startJob.func1(0xc00005c0a0, 0x514d20, 0xc000070390)
D:/code/golang/pkg/mod/github.com/robfig/cron/v3@v3.0.1/cron.go:312 +0x68
created by github.com/robfig/cron/v3.(*Cron).startJob
D:/code/golang/pkg/mod/github.com/robfig/cron/v3@v3.0.1/cron.go:310 +0x7a
hello world
hello world
hello world
hello world
Посмотримcron.Recover()
Реализация очень проста:
// cron.go
func Recover(logger Logger) JobWrapper {
return func(j Job) Job {
return FuncJob(func() {
defer func() {
if r := recover(); r != nil {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
err, ok := r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
logger.Error(err, "panic", "stack", "...\n"+string(buf))
}
}()
j.Run()
})
}
}
выполняет внутренний слойJob
Перед логикой добавитьrecover()
перечислить. еслиJob.Run()
во время исполненияpanic
. здесьrecover()
Будет захватывать, выводить стек вызовов.
DelayIfStillRunning
Давайте сначала посмотрим, как его использовать:
type delayJob struct {
count int
}
func (d *delayJob) Run() {
time.Sleep(2 * time.Second)
d.count++
log.Printf("%d: hello world\n", d.count)
}
func main() {
c := cron.New()
c.AddJob("@every 1s", cron.NewChain(cron.DelayIfStillRunning(cron.DefaultLogger)).Then(&delayJob{}))
c.Start()
time.Sleep(10 * time.Second)
}
выше мыRun()
К выходу добавляется задержка в 2 с, и интервал на выходе становится равным 2 с вместо рассчитанных по времени 1 с:
$ go run main.go
2020/06/27 14:11:16 1: hello world
2020/06/27 14:11:18 2: hello world
2020/06/27 14:11:20 3: hello world
2020/06/27 14:11:22 4: hello world
Взгляните на исходный код:
// chain.go
func DelayIfStillRunning(logger Logger) JobWrapper {
return func(j Job) Job {
var mu sync.Mutex
return FuncJob(func() {
start := time.Now()
mu.Lock()
defer mu.Unlock()
if dur := time.Since(start); dur > time.Minute {
logger.Info("delay", "duration", dur)
}
j.Run()
})
}
}
Сначала определите мьютекс, совместно используемый задачейsync.Mutex
, блокировка устанавливается перед выполнением каждой задачи и снимается после завершения выполнения. Следовательно, до завершения предыдущей задачи следующая задача не может успешно получить блокировку, что обеспечивает последовательное выполнение задач.
SkipIfStillRunning
Давайте сначала посмотрим, как его использовать:
type skipJob struct {
count int32
}
func (d *skipJob) Run() {
atomic.AddInt32(&d.count, 1)
log.Printf("%d: hello world\n", d.count)
if atomic.LoadInt32(&d.count) == 1 {
time.Sleep(2 * time.Second)
}
}
func main() {
c := cron.New()
c.AddJob("@every 1s", cron.NewChain(cron.SkipIfStillRunning(cron.DefaultLogger)).Then(&skipJob{}))
c.Start()
time.Sleep(10 * time.Second)
}
вывод:
$ go run main.go
2020/06/27 14:22:07 1: hello world
2020/06/27 14:22:10 2: hello world
2020/06/27 14:22:11 3: hello world
2020/06/27 14:22:12 4: hello world
2020/06/27 14:22:13 5: hello world
2020/06/27 14:22:14 6: hello world
2020/06/27 14:22:15 7: hello world
2020/06/27 14:22:16 8: hello world
Обратите внимание на время наблюдения: разница между первым и вторым выводом составляет 3 с, поскольку два выполнения пропускаются.
УведомлениеDelayIfStillRunning
а такжеSkipIfStillRunning
принципиально отличается от прежнегоDelayIfStillRunning
Пока времени достаточно, все задачи будут выполняться шаг за шагом, но предыдущая задача может занять слишком много времени, что приведет к небольшой задержке времени выполнения последней задачи.SkipIfStillRunning
Некоторые выполнения будут пропущены.
Взгляните на исходный код:
func SkipIfStillRunning(logger Logger) JobWrapper {
return func(j Job) Job {
var ch = make(chan struct{}, 1)
ch <- struct{}{}
return FuncJob(func() {
select {
case v := <-ch:
j.Run()
ch <- v
default:
logger.Info("skip")
}
})
}
}
Определите канал с размером буфера 1, общим для задачи.chan struct{}
. При выполнении задачи получить значение из канала, в случае успеха выполнить, иначе пропустить. После завершения выполнения отправьте значение в канал, чтобы убедиться, что следующая задача может быть выполнена. Изначально отправляет в канал значение, гарантирующее выполнение первой задачи.
Суммировать
cron
Реализация относительно небольшая и элегантная, а количество строк кода невелико, что очень стоит посмотреть!
Если вы найдете забавную и простую в использовании языковую библиотеку Go, добро пожаловать, чтобы отправить вопрос на Go Daily Library GitHub😄
Ссылаться на
- крон на гитхабе:github.com/robfig/cron
- Ежедневная библиотека Carbon of Go:Дарен Джун.GitHub.IO/2020/02/14/…
- Ежедневная библиотека Грона Го:Дарен Джун.GitHub.IO/2020/04/20/…
- Перейти на ежедневный репозиторий GitHub:GitHub.com/Darenjun/go-of…
я
мой блог:darjun.github.io
Добро пожаловать, чтобы обратить внимание на мою общедоступную учетную запись WeChat [GoUpUp], учитесь вместе и добивайтесь прогресса вместе ~