Xây dựng bộ lập lịch tự động - Cron job với Golang
I. Giới thiệu
Bài viết sẽ gồm 3 phần, giới thiệu, viết cron job cơ bản và cấu trúc một cron job server hoàn chỉnh.
Cron Job là chương trình lập lịch, tự động thực hiện một số công việc nào đó như:
Tự động sao lưu dữ liệu.
Gửi email, thông báo hằng ngày.
Với Golang, bạn có thể tạo và quản lý cron job bằng cách sử dụng thư viện như robfig/cron
hay teambition/rrule-go
(dành cho bạn nào muốn build chương trình giống google calendar).
Trong bài viết này mình sẽ dùng thư viện robfig/cron
để xây dựng bộ lập lịch thông báo tới người dùng. Bạn có thể xem source code chi tiết tại đây
II. Cấu trúc cơ bản
Một số cú pháp
Các cú pháp của cron job được ký hiệu bằng 5 trường để xác định thời gian. Bạn có thể xem chi tiết và kiểm tra cú pháp trên trang crontab.guru
Tạo cronjob cơ bản
package main
import (
"fmt"
"github.com/robfig/cron/v3"
)
func main() {
c := cron.New()
// Thêm một công việc vào cron
c.AddFunc("* 5 * * *", func() {
fmt.Println("Dậy đi ông cháu ơi")
})
// Khởi động cron
c.Start()
// Giữ chương trình hoạt động, cho đến khi ai đó tắt server
var forever chan struct{}
<-forever
}
III. Thiết kế server
Viết như bên trên là đã hoàn thành một cron job cơ bản rồi. Tuy nhiên nó còn sơ sài, nên mình sẽ cấu trúc lại nó thành một server hoàn chỉnh và dễ dàng thêm các job (việc cần làm) vào hơn.
Mình thiết kế cron-job server với 2 nhiệm vụ chính:Đọc danh sách công việc được ghi trong file (mỗi lần thêm job bạn chỉ cần thêm vào đây là xong)
Gọi API tới main server để gửi thông báo tới người dùng
Bên trong cron-job server sẽ gồm:
- Dưới đây là code chi tiết của các file:
main.go
- File chạy chính của ứng dụng, điểm bắt đầu khi khởi chạy ứng dụng của mình.
package main
import "cron-job/service"
func main() {
app := service.NewApp()
app.Run()
}
service/service.go
- Xử lý logic chính, chịu trách nhiệm đăng ký các job cần làm và run app.
package service
import (
"cron-job/conf"
"cron-job/handler"
"cron-job/model"
"errors"
"fmt"
"github.com/caarlos0/env/v6"
"github.com/robfig/cron/v3"
)
type App struct {
cronjob *cron.Cron //
configJobs []*model.JobConfig // các job được đọc từ file sẽ lưu vào đây
}
func NewApp() *App {
return &App{
cronjob: cron.New(), // khởi tạo cronjob
}
}
func (a *App) registerJobs() error {
for i, jobCfg := range a.configJobs {
// fill a name if empty
if jobCfg.Name == "" {
jobCfg.Name = fmt.Sprintf("job-%d", i)
}
handler := handler.NewHandler(jobCfg)
// tạo ra job lập lịch dựa theo jobCfg.Spec và gọi đến api cần thiết
_, err := a.cronjob.AddJob(jobCfg.Spec, handler)
if err != nil {
return fmt.Errorf("register job error (name: %s): %v", jobCfg.Name, err)
}
}
return nil
}
func (a *App) Run() error {
config := conf.NewConfig()
err := env.Parse(config)
if err != nil {
return errors.New("error while parsing extra setting: " + err.Error())
}
a.configJobs, err = config.LoadConfigJobs()
if err != nil {
return fmt.Errorf("load jobs error: %v", err)
}
if err = a.registerJobs(); err != nil {
return fmt.Errorf("failed to register jobs: %v", err)
}
a.cronjob.Start()
// Giữ chương trình hoạt động, cho đến khi ai đó tắt server
var forever chan struct{}
<-forever
return nil
}
handler/handler.go
- Xử lý yêu cầu từ service và gọi tới api của main server
package handler
import (
"context"
"cron-job/model"
"fmt"
"net/http"
"strings"
"sync"
"time"
)
type Handler struct {
client *http.Client
cfg *model.JobConfig
initOnce sync.Once // Đảm bảo hàm init chỉ được gọi 1 lần dù có nhiều goroutine gọi
}
func NewHandler(cfg *model.JobConfig) *Handler {
return &Handler{
cfg: cfg,
}
}
func (h *Handler) init() {
if h.client == nil {
h.client = &http.Client{
// Không cho điều hướng
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
if h.cfg.HandlerConfig.TimeoutSeconds == 0 {
h.cfg.HandlerConfig.TimeoutSeconds = 10
}
}
func (h *Handler) Run() {
h.initOnce.Do(h.init)
cfg := h.cfg.HandlerConfig
timestamp := time.Now()
timeout := time.Duration(cfg.TimeoutSeconds) * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, cfg.Method, cfg.URL, strings.NewReader(cfg.Body))
if err != nil {
return
}
for k, v := range cfg.Headers {
req.Header.Set(k, v)
}
req.Header.Set("x-time", fmt.Sprintf("%d", timestamp.Unix()))
_, err = h.client.Do(req)
if err != nil {
return
}
}
config/config.go
- Đọc các việc cần làm từ file yml, lưu vào model
package conf
import (
"cron-job/model"
"fmt"
"gopkg.in/yaml.v3"
"io/ioutil"
)
type Config struct {
ConfigFile string `env:"CONFIG_FILE" envDefault:"config.yml"`
}
func NewConfig() *Config {
return &Config{}
}
// Đọc các việc cần làm từ file yml
func (c *Config) LoadConfigJobs() ([]*model.JobConfig, error) {
raw, err := ioutil.ReadFile(c.ConfigFile)
if err != nil {
return nil, fmt.Errorf("failed read config file: %v", err)
}
var jobs []*model.JobConfig
err = yaml.Unmarshal(raw, &jobs)
if err != nil {
return nil, fmt.Errorf("failed to decode config data: %v", err)
}
return jobs, nil
}
config/config.yml
- Định nghĩa các công việc cần thực hiện gồm: tên, thời gian gọi api, chi tiết api nào sẽ được gọi...
- name: "đón bạn gái"
spec: "* 11 * * *" # 11h mỗi ngày
handler:
method: POST
url: http://localhost:8000/pick-up-girlfriend
headers:
content-type: application/json
body: |
{}
model/job.go
- Định nghĩa struct để lưu các dữ liệu của file yml.
package model
// JobConfig represents a job configuration at a time
type JobConfig struct {
Name string `yaml:"name"`
Spec string `yaml:"spec"`
HandlerConfig *HandlerHttp `yaml:"handler"`
}
// HandlerHttp represents a http handler configuration
type HandlerHttp struct {
Method string `yaml:"method"`
URL string `yaml:"url"`
Headers map[string]string `yaml:"headers"`
Body string `yaml:"body"`
TimeoutSeconds int `yaml:"timeout_seconds"`
}
IV. Ngoài lề
- Tụi mình có một kênh discord về công nghệ, việc làm, phỏng vấn tên là Code club Vietnam. Ai có hứng thú có thể join để cùng thảo luận nha.