From 61208f524768e7ef0ee5424dbc41a61ad3246f2a Mon Sep 17 00:00:00 2001 From: eleksir Date: Sun, 2 Nov 2025 12:22:37 +0300 Subject: [PATCH] [WIP] Add cron-like job for periodic maintenance of settings db, update TODO.md --- TODO.md | 11 +- cmd/aleesa-telegram-go/main.go | 8 + go.mod | 1 + go.sum | 2 + internal/tg/globals.go | 4 + internal/tg/periodicJobs.go | 23 ++ internal/tg/sigHandler.go | 14 +- .../carlescere/scheduler/.gitignore | 27 ++ .../carlescere/scheduler/.travis.yml | 16 + .../github.com/carlescere/scheduler/LICENSE | 22 ++ .../github.com/carlescere/scheduler/README.md | 58 ++++ .../carlescere/scheduler/scheduler.go | 327 ++++++++++++++++++ vendor/modules.txt | 3 + 13 files changed, 504 insertions(+), 12 deletions(-) create mode 100644 internal/tg/periodicJobs.go create mode 100644 vendor/github.com/carlescere/scheduler/.gitignore create mode 100644 vendor/github.com/carlescere/scheduler/.travis.yml create mode 100644 vendor/github.com/carlescere/scheduler/LICENSE create mode 100644 vendor/github.com/carlescere/scheduler/README.md create mode 100644 vendor/github.com/carlescere/scheduler/scheduler.go diff --git a/TODO.md b/TODO.md index cd413cf..a21e0db 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,7 @@ В качестве основы возьмём https://github.com/NicoNex/echotron Для реализации телеграммного фронтэнда нам надо сделать: --1. Перелелать layout на "стандартный" гошный. +-1. [V] Переделать layout на "стандартный" гошный. 0. Парсер конфига. @@ -25,7 +25,7 @@ 1 [V] [V] цензор сообщений определённого типа. - 2 [ ] [V] фортунка каждое утро. (+ добавить время, относительно gmt, когда показывать фортунку). + 2 [ ] [V] фортунка каждое утро. (+ добавить время, относительно gmt, когда показывать фортунку). См. п.13. 3 [V] [V] Приветствие новых участников @@ -63,5 +63,12 @@ 12. [ ] промоут/демоут участника до админа +13. [ ] cron-like "сервис". Он нужен для + [ ] отправки приветствий по утрам. + [V] регулярных компакций базы с настройками. + [ ] регулярного обслуживания таблиц с пользователями. + + + frontend -> router -> service -> frontend diff --git a/cmd/aleesa-telegram-go/main.go b/cmd/aleesa-telegram-go/main.go index edd0633..3206e45 100644 --- a/cmd/aleesa-telegram-go/main.go +++ b/cmd/aleesa-telegram-go/main.go @@ -11,6 +11,7 @@ import ( "aleesa-telegram-go/internal/log" "aleesa-telegram-go/internal/tg" + "github.com/carlescere/scheduler" "github.com/go-redis/redis/v8" ) @@ -88,6 +89,13 @@ func main() { go tg.SigHandler() go tg.Telega(tg.Config) + // Periodically compact/flush pebble dbs. + if job, err := scheduler.Every(1).Hours().NotImmediately().Run(tg.TidySettingsDB); err != nil { + log.Errorf("Unable to schedule periodic settings db flush: %s", err) + } else { + tg.PeriodicJobs = append(tg.PeriodicJobs, job) + } + // Обработчик событий от редиски. for msg := range redisMsgChan { if tg.Shutdown { diff --git a/go.mod b/go.mod index 9cb2c3b..e76ecfc 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25 require ( github.com/NicoNex/echotron/v3 v3.33.1 + github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 github.com/cockroachdb/errors v1.11.3 github.com/cockroachdb/pebble v1.1.1 github.com/davecgh/go-spew v1.1.1 diff --git a/go.sum b/go.sum index f76269a..3e9d7e6 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/NicoNex/echotron/v3 v3.33.1 h1:P9bVvmnuVd0C7bR5q9TV6Pece/wBqZ6PSo0Rqo github.com/NicoNex/echotron/v3 v3.33.1/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 h1:9bAydALqAjBfPHd/eAiJBHnMZUYov8m2PkXVr+YGQeI= +github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82/go.mod h1:tyA14J0sA3Hph4dt+AfCjPrYR13+vVodshQSM7km9qw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= diff --git a/internal/tg/globals.go b/internal/tg/globals.go index dfcb98c..db8c4dc 100644 --- a/internal/tg/globals.go +++ b/internal/tg/globals.go @@ -5,6 +5,7 @@ import ( "os" "github.com/NicoNex/echotron/v3" + "github.com/carlescere/scheduler" "github.com/cockroachdb/pebble" "github.com/go-redis/redis/v8" ) @@ -45,6 +46,9 @@ var ( "Добро пожаловать в наше скромное коммунити", "Наше вам с кисточкой тут, на канальчике", } + + // PeriodicJobs contains pointer to slice with sheduled job objects. + PeriodicJobs = make([]*scheduler.Job, 0) ) /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/tg/periodicJobs.go b/internal/tg/periodicJobs.go new file mode 100644 index 0000000..eaf26a8 --- /dev/null +++ b/internal/tg/periodicJobs.go @@ -0,0 +1,23 @@ +package tg + +import ( + "aleesa-telegram-go/internal/log" +) + +var ( + // TidySettingsDB flushes regularly pebble dbs with settings. Remmended action to keeb that db in a good shape. + // Meant to be run about hourly or something like that. + TidySettingsDB = func() { + if len(settingsDB) > 0 { + for name, db := range settingsDB { + log.Debugf("Flushing %s settings db", name) + + if err := db.Flush(); err != nil { + log.Errorf("Unable to Flush() %s db: %s", name, err) + } + } + } + } +) + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/tg/sigHandler.go b/internal/tg/sigHandler.go index eda7b8d..43bdb0b 100644 --- a/internal/tg/sigHandler.go +++ b/internal/tg/sigHandler.go @@ -42,18 +42,12 @@ func SigHandler() { log.Debug("Close redis connection") } - // close all telego stuff. - resp, err := tg.LogOut() - - if err != nil { - log.Errorf("Unable to send LogOut() to Telegram Bot API server") + // Quit all scheduled periodic jobs. + for _, job := range PeriodicJobs { + job.Quit <- true } - if !resp.Ok { - log.Errorf("Telegram Bot API returns an error on LogOut(): %d, %s", resp.ErrorCode, resp.Description) - } - - resp, err = tg.Close() + resp, err := tg.Close() if err != nil { log.Errorf("Unable to send Close() to Telegram Bot API server") diff --git a/vendor/github.com/carlescere/scheduler/.gitignore b/vendor/github.com/carlescere/scheduler/.gitignore new file mode 100644 index 0000000..2fb904f --- /dev/null +++ b/vendor/github.com/carlescere/scheduler/.gitignore @@ -0,0 +1,27 @@ +# Editor stuff +*.swp + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/carlescere/scheduler/.travis.yml b/vendor/github.com/carlescere/scheduler/.travis.yml new file mode 100644 index 0000000..e3b7f32 --- /dev/null +++ b/vendor/github.com/carlescere/scheduler/.travis.yml @@ -0,0 +1,16 @@ +language: go +go: + - 1.2 + - 1.3 + - 1.4 + - 1.5 + - 1.6 + +install: + - go get github.com/axw/gocov/gocov + - go get github.com/mattn/goveralls + - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi + - go get github.com/stretchr/testify/assert +script: + - $HOME/gopath/bin/goveralls -service=travis-ci -repotoken $COVERALLS_TOKEN + - go test -race diff --git a/vendor/github.com/carlescere/scheduler/LICENSE b/vendor/github.com/carlescere/scheduler/LICENSE new file mode 100644 index 0000000..f889f05 --- /dev/null +++ b/vendor/github.com/carlescere/scheduler/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Carles Cerezo Guzmán + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/carlescere/scheduler/README.md b/vendor/github.com/carlescere/scheduler/README.md new file mode 100644 index 0000000..23d5ed4 --- /dev/null +++ b/vendor/github.com/carlescere/scheduler/README.md @@ -0,0 +1,58 @@ +# scheduler +[![GoDoc](https://godoc.org/github.com/carlescere/scheduler?status.svg)](https://godoc.org/github.com/carlescere/scheduler) +[![Build Status](https://travis-ci.org/carlescere/scheduler.svg?branch=master)](https://travis-ci.org/carlescere/scheduler) +[![Coverage Status](https://coveralls.io/repos/carlescere/scheduler/badge.svg?branch=master)](https://coveralls.io/r/carlescere/scheduler?branch=master) + +Job scheduling made easy. + +Scheduler allows you to schedule recurrent jobs with an easy-to-read syntax. + +Inspired by the article **[Rethinking Cron](http://adam.heroku.com/past/2010/4/13/rethinking_cron/)** and the **[schedule](https://github.com/dbader/schedule)** python module. + +## How to use? +```go +package main + +import ( + "fmt" + "runtime" + "time" + + "github.com/carlescere/scheduler" +) + +func main() { + job := func() { + t := time.Now() + fmt.Println("Time's up! @", t.UTC()) + } + // Run every 2 seconds but not now. + scheduler.Every(2).Seconds().NotImmediately().Run(job) + + // Run now and every X. + scheduler.Every(5).Minutes().Run(job) + scheduler.Every().Day().Run(job) + scheduler.Every().Monday().At("08:30").Run(job) + + // Keep the program from not exiting. + runtime.Goexit() +} +``` + +## How it works? +By specifying the chain of calls, a `Job` struct is instantiated and a goroutine is starts observing the `Job`. + +The goroutine will be on pause until: +* The next run scheduled is due. This will cause to execute the job. +* The `SkipWait` channel is activated. This will cause to execute the job. +* The `Quit` channel is activated. This will cause to finish the job. + +## Not immediate recurrent jobs +By default the behaviour of the recurrent jobs (Every(N) seconds, minutes, hours) is to start executing the job right away and then wait the required amount of time. By calling specifically `.NotImmediately()` you can override that behaviour and not execute it directly when the function `Run()` is called. + +```go +scheduler.Every(5).Minutes().NotImmediately().Run(job) +``` + +## License +Distributed under MIT license. See `LICENSE` for more information. diff --git a/vendor/github.com/carlescere/scheduler/scheduler.go b/vendor/github.com/carlescere/scheduler/scheduler.go new file mode 100644 index 0000000..13f2f90 --- /dev/null +++ b/vendor/github.com/carlescere/scheduler/scheduler.go @@ -0,0 +1,327 @@ +// Package scheduler is a cron replacement based on: +// http://adam.herokuapp.com/past/2010/4/13/rethinking_cron/ +// and +// https://github.com/dbader/schedule +// +// Uses include: +// func main() { +// job := func() { +// fmt.Println("Time's up!") +// } +// scheduler.Every(5).Seconds().Run(function) +// scheduler.Every().Day().Run(function) +// scheduler.Every().Sunday().At("08:30").Run(function) +// } +package scheduler + +import ( + "errors" + "strconv" + "strings" + "sync" + "time" +) + +type scheduled interface { + nextRun() (time.Duration, error) +} + +// Job defines a running job and allows to stop a scheduled job or run it. +type Job struct { + fn func() + Quit chan bool + SkipWait chan bool + err error + schedule scheduled + isRunning bool + sync.RWMutex +} + +type recurrent struct { + units int + period time.Duration + done bool +} + +func (r *recurrent) nextRun() (time.Duration, error) { + if r.units == 0 || r.period == 0 { + return 0, errors.New("cannot set recurrent time with 0") + } + if !r.done { + r.done = true + return 0, nil + } + return time.Duration(r.units) * r.period, nil +} + +type daily struct { + hour int + min int + sec int +} + +func (d *daily) setTime(h, m, s int) { + d.hour = h + d.min = m + d.sec = s +} + +func (d daily) nextRun() (time.Duration, error) { + now := time.Now() + year, month, day := now.Date() + date := time.Date(year, month, day, d.hour, d.min, d.sec, 0, time.Local) + if now.Before(date) { + return date.Sub(now), nil + } + date = time.Date(year, month, day+1, d.hour, d.min, d.sec, 0, time.Local) + return date.Sub(now), nil +} + +type weekly struct { + day time.Weekday + d daily +} + +func (w weekly) nextRun() (time.Duration, error) { + now := time.Now() + year, month, day := now.Date() + numDays := w.day - now.Weekday() + if numDays == 0 { + numDays = 7 + } else if numDays < 0 { + numDays += 7 + } + date := time.Date(year, month, day+int(numDays), w.d.hour, w.d.min, w.d.sec, 0, time.Local) + return date.Sub(now), nil +} + +// Every defines when to run a job. For a recurrent jobs (n seconds/minutes/hours) you +// should specify the unit and then call to the correspondent period method. +func Every(times ...int) *Job { + switch len(times) { + case 0: + return &Job{} + case 1: + r := new(recurrent) + r.units = times[0] + return &Job{schedule: r} + default: + // Yeah... I don't like it either. But go does not support default + // parameters nor method overloading. In an ideal world should + // return an error at compile time not at runtime. :/ + return &Job{err: errors.New("too many arguments in Every")} + } +} + +// NotImmediately allows recurrent jobs not to be executed immediatelly after +// definition. If a job is declared hourly won't start executing until the first hour +// passed. +func (j *Job) NotImmediately() *Job { + rj, ok := j.schedule.(*recurrent) + if !ok { + j.err = errors.New("bad function chaining") + return j + } + rj.done = true + return j +} + +// At lets you define a specific time when the job would be run. Does not work with +// recurrent jobs. +// Time should be defined as a string separated by a colon. Could be used as "08:35:30", +// "08:35" or "8" for only the hours. +func (j *Job) At(hourTime string) *Job { + if j.err != nil { + return j + } + hour, min, sec, err := parseTime(hourTime) + if err != nil { + j.err = err + return j + } + d, ok := j.schedule.(daily) + if !ok { + w, ok := j.schedule.(weekly) + if !ok { + j.err = errors.New("bad function chaining") + return j + } + w.d.setTime(hour, min, sec) + j.schedule = w + } else { + d.setTime(hour, min, sec) + j.schedule = d + } + return j +} + +// Run sets the job to the schedule and returns the pointer to the job so it may be +// stopped or executed without waiting or an error. +func (j *Job) Run(f func()) (*Job, error) { + if j.err != nil { + return nil, j.err + } + var next time.Duration + var err error + j.Quit = make(chan bool, 1) + j.SkipWait = make(chan bool, 1) + j.fn = f + // Check for possible errors in scheduling + next, err = j.schedule.nextRun() + if err != nil { + return nil, err + } + go func(j *Job) { + for { + select { + case <-j.Quit: + return + case <-j.SkipWait: + go runJob(j) + case <-time.After(next): + go runJob(j) + } + next, _ = j.schedule.nextRun() + } + }(j) + return j, nil +} + +func (j *Job) setRunning(running bool) { + j.Lock() + defer j.Unlock() + + j.isRunning = running +} + +func runJob(job *Job) { + if job.IsRunning() { + return + } + job.setRunning(true) + job.fn() + job.setRunning(false) +} + +func parseTime(str string) (hour, min, sec int, err error) { + chunks := strings.Split(str, ":") + var hourStr, minStr, secStr string + switch len(chunks) { + case 1: + hourStr = chunks[0] + minStr = "0" + secStr = "0" + case 2: + hourStr = chunks[0] + minStr = chunks[1] + secStr = "0" + case 3: + hourStr = chunks[0] + minStr = chunks[1] + secStr = chunks[2] + } + hour, err = strconv.Atoi(hourStr) + if err != nil { + return 0, 0, 0, errors.New("bad time") + } + min, err = strconv.Atoi(minStr) + if err != nil { + return 0, 0, 0, errors.New("bad time") + } + sec, err = strconv.Atoi(secStr) + if err != nil { + return 0, 0, 0, errors.New("bad time") + } + + if hour > 23 || min > 59 || sec > 59 { + return 0, 0, 0, errors.New("bad time") + } + + return +} + +func (j *Job) dayOfWeek(d time.Weekday) *Job { + if j.schedule != nil { + j.err = errors.New("bad function chaining") + } + j.schedule = weekly{day: d} + return j +} + +// Monday sets the job to run every Monday. +func (j *Job) Monday() *Job { + return j.dayOfWeek(time.Monday) +} + +// Tuesday sets the job to run every Tuesday. +func (j *Job) Tuesday() *Job { + return j.dayOfWeek(time.Tuesday) +} + +// Wednesday sets the job to run every Wednesday. +func (j *Job) Wednesday() *Job { + return j.dayOfWeek(time.Wednesday) +} + +// Thursday sets the job to run every Thursday. +func (j *Job) Thursday() *Job { + return j.dayOfWeek(time.Thursday) +} + +// Friday sets the job to run every Friday. +func (j *Job) Friday() *Job { + return j.dayOfWeek(time.Friday) +} + +// Saturday sets the job to run every Saturday. +func (j *Job) Saturday() *Job { + return j.dayOfWeek(time.Saturday) +} + +// Sunday sets the job to run every Sunday. +func (j *Job) Sunday() *Job { + return j.dayOfWeek(time.Sunday) +} + +// Day sets the job to run every day. +func (j *Job) Day() *Job { + if j.schedule != nil { + j.err = errors.New("bad function chaining") + } + j.schedule = daily{} + return j +} + +func (j *Job) timeOfDay(d time.Duration) *Job { + if j.err != nil { + return j + } + r := j.schedule.(*recurrent) + r.period = d + j.schedule = r + return j +} + +// Seconds sets the job to run every n Seconds where n was defined in the Every +// function. +func (j *Job) Seconds() *Job { + return j.timeOfDay(time.Second) +} + +// Minutes sets the job to run every n Minutes where n was defined in the Every +// function. +func (j *Job) Minutes() *Job { + return j.timeOfDay(time.Minute) +} + +// Hours sets the job to run every n Hours where n was defined in the Every function. +func (j *Job) Hours() *Job { + return j.timeOfDay(time.Hour) +} + +// IsRunning returns if the job is currently running +func (j *Job) IsRunning() bool { + j.RLock() + defer j.RUnlock() + return j.isRunning +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 0dc3fa4..0f9459a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -7,6 +7,9 @@ github.com/NicoNex/echotron/v3 # github.com/beorn7/perks v1.0.1 ## explicit; go 1.11 github.com/beorn7/perks/quantile +# github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 +## explicit +github.com/carlescere/scheduler # github.com/cespare/xxhash/v2 v2.3.0 ## explicit; go 1.11 github.com/cespare/xxhash/v2