0

[WIP] Add cron-like job for periodic maintenance of settings db, update TODO.md

This commit is contained in:
eleksir
2025-11-02 12:22:37 +03:00
parent fbd0196a48
commit 61208f5247
13 changed files with 504 additions and 12 deletions

11
TODO.md
View File

@@ -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

View File

@@ -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 {

1
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=

View File

@@ -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: */

View File

@@ -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: */

View File

@@ -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")

27
vendor/github.com/carlescere/scheduler/.gitignore generated vendored Normal file
View File

@@ -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

16
vendor/github.com/carlescere/scheduler/.travis.yml generated vendored Normal file
View File

@@ -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

22
vendor/github.com/carlescere/scheduler/LICENSE generated vendored Normal file
View File

@@ -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.

58
vendor/github.com/carlescere/scheduler/README.md generated vendored Normal file
View File

@@ -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.

327
vendor/github.com/carlescere/scheduler/scheduler.go generated vendored Normal file
View File

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

3
vendor/modules.txt vendored
View File

@@ -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