Go ежедневная библиотека cron

Go

Введение

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)
}

Очень прост в использовании, создайтеcronObject, этот объект используется для управления временными задачами.

перечислить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😄

Ссылаться на

  1. крон на гитхабе:github.com/robfig/cron
  2. Ежедневная библиотека Carbon of Go:Дарен Джун.GitHub.IO/2020/02/14/…
  3. Ежедневная библиотека Грона Го:Дарен Джун.GitHub.IO/2020/04/20/…
  4. Перейти на ежедневный репозиторий GitHub:GitHub.com/Darenjun/go-of…

я

мой блог:darjun.github.io

Добро пожаловать, чтобы обратить внимание на мою общедоступную учетную запись WeChat [GoUpUp], учитесь вместе и добивайтесь прогресса вместе ~