apollo-backend/internal/cmd/scheduler.go

531 lines
12 KiB
Go
Raw Normal View History

package cmd
2021-07-08 23:03:46 +00:00
import (
"context"
2021-07-09 03:12:50 +00:00
"encoding/json"
2021-07-08 23:03:46 +00:00
"fmt"
"strconv"
2021-07-08 23:03:46 +00:00
"time"
2021-07-09 02:09:14 +00:00
"github.com/DataDog/datadog-go/statsd"
2021-07-08 23:03:46 +00:00
"github.com/adjust/rmq/v4"
"github.com/go-co-op/gocron"
"github.com/go-redis/redis/v8"
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
2021-08-14 16:08:17 +00:00
2022-03-28 21:05:01 +00:00
"github.com/christianselig/apollo-backend/internal/cmdutil"
"github.com/christianselig/apollo-backend/internal/domain"
"github.com/christianselig/apollo-backend/internal/repository"
2021-07-09 03:12:50 +00:00
)
2022-03-28 21:05:01 +00:00
const batchSize = 250
func SchedulerCmd(ctx context.Context) *cobra.Command {
cmd := &cobra.Command{
Use: "scheduler",
Args: cobra.ExactArgs(0),
Short: "Schedules jobs and runs several maintenance tasks periodically.",
RunE: func(cmd *cobra.Command, args []string) error {
logger := cmdutil.NewLogrusLogger(false)
statsd, err := cmdutil.NewStatsdClient()
if err != nil {
return err
}
defer statsd.Close()
2021-07-20 17:00:53 +00:00
db, err := cmdutil.NewDatabasePool(ctx, 1)
if err != nil {
return err
}
defer db.Close()
redis, err := cmdutil.NewRedisClient(ctx)
if err != nil {
return err
}
defer redis.Close()
queue, err := cmdutil.NewQueueClient(logger, redis, "worker")
if err != nil {
return err
}
// Eval lua so that we don't keep parsing it
luaSha, err := evalScript(ctx, redis)
if err != nil {
return err
}
notifQueue, err := queue.OpenQueue("notifications")
if err != nil {
return err
}
2021-09-25 16:56:01 +00:00
subredditQueue, err := queue.OpenQueue("subreddits")
if err != nil {
return err
}
2021-10-10 15:51:42 +00:00
trendingQueue, err := queue.OpenQueue("trending")
if err != nil {
return err
}
2021-10-09 14:59:20 +00:00
userQueue, err := queue.OpenQueue("users")
if err != nil {
return err
}
2021-10-17 14:17:41 +00:00
stuckNotificationsQueue, err := queue.OpenQueue("stuck-notifications")
if err != nil {
return err
}
s := gocron.NewScheduler(time.UTC)
2022-05-22 23:35:03 +00:00
_, _ = s.Every(500).Milliseconds().SingletonMode().Do(func() { enqueueAccounts(ctx, logger, statsd, db, redis, luaSha, notifQueue) })
_, _ = s.Every(500).Milliseconds().SingletonMode().Do(func() { enqueueSubreddits(ctx, logger, statsd, db, []rmq.Queue{subredditQueue, trendingQueue}) })
_, _ = s.Every(500).Milliseconds().SingletonMode().Do(func() { enqueueUsers(ctx, logger, statsd, db, userQueue) })
_, _ = s.Every(1).Second().Do(func() { cleanQueues(logger, queue) })
2021-10-17 14:17:41 +00:00
_, _ = s.Every(1).Second().Do(func() { enqueueStuckAccounts(ctx, logger, statsd, db, stuckNotificationsQueue) })
_, _ = s.Every(1).Minute().Do(func() { reportStats(ctx, logger, statsd, db) })
2021-09-25 13:19:42 +00:00
_, _ = s.Every(1).Minute().Do(func() { pruneAccounts(ctx, logger, db) })
_, _ = s.Every(1).Minute().Do(func() { pruneDevices(ctx, logger, db) })
s.StartAsync()
<-ctx.Done()
s.Stop()
return nil
},
2021-07-08 23:03:46 +00:00
}
return cmd
2021-07-08 23:03:46 +00:00
}
func evalScript(ctx context.Context, redis *redis.Client) (string, error) {
lua := fmt.Sprintf(`
local retv={}
local ids=cjson.decode(ARGV[1])
for i=1, #ids do
local key = KEYS[1] .. ":" .. ids[i]
if redis.call("exists", key) == 0 then
redis.call("setex", key, %d, 1)
retv[#retv + 1] = ids[i]
end
end
return retv
2022-03-28 21:05:01 +00:00
`, int64(domain.NotificationCheckTimeout.Seconds()))
return redis.ScriptLoad(ctx, lua).Result()
}
2021-08-14 15:59:13 +00:00
func pruneAccounts(ctx context.Context, logger *logrus.Logger, pool *pgxpool.Pool) {
2022-03-28 21:05:01 +00:00
expiry := time.Now().Add(-domain.StaleTokenThreshold)
2021-08-14 15:54:48 +00:00
ar := repository.NewPostgresAccount(pool)
2022-03-28 21:05:01 +00:00
stale, err := ar.PruneStale(ctx, expiry)
2021-08-14 15:54:48 +00:00
if err != nil {
logger.WithFields(logrus.Fields{
"err": err,
}).Error("failed cleaning stale accounts")
return
}
2021-08-14 15:59:13 +00:00
orphaned, err := ar.PruneOrphaned(ctx)
2021-07-12 19:36:22 +00:00
if err != nil {
logger.WithFields(logrus.Fields{
"err": err,
2021-08-14 15:59:13 +00:00
}).Error("failed cleaning orphaned accounts")
2021-07-12 19:36:22 +00:00
return
}
if count := stale + orphaned; count > 0 {
2021-07-23 00:22:46 +00:00
logger.WithFields(logrus.Fields{
2022-03-28 21:05:01 +00:00
"stale": stale,
"orphaned": orphaned,
2021-08-14 15:59:13 +00:00
}).Info("pruned accounts")
2021-07-23 00:22:46 +00:00
}
2021-07-12 19:36:22 +00:00
}
2021-08-14 16:08:17 +00:00
func pruneDevices(ctx context.Context, logger *logrus.Logger, pool *pgxpool.Pool) {
2022-03-28 21:05:01 +00:00
now := time.Now()
2021-08-14 16:08:17 +00:00
dr := repository.NewPostgresDevice(pool)
2022-03-28 21:05:01 +00:00
count, err := dr.PruneStale(ctx, now)
2021-08-14 16:08:17 +00:00
if err != nil {
logger.WithFields(logrus.Fields{
"err": err,
}).Error("failed cleaning stale devices")
return
}
if count > 0 {
logger.WithFields(logrus.Fields{
"count": count,
}).Info("pruned devices")
}
}
func cleanQueues(logger *logrus.Logger, jobsConn rmq.Connection) {
2021-07-12 19:36:22 +00:00
cleaner := rmq.NewCleaner(jobsConn)
count, err := cleaner.Clean()
if err != nil {
logger.WithFields(logrus.Fields{
"err": err,
}).Error("failed cleaning jobs from queues")
return
}
2021-10-17 14:17:41 +00:00
if count > 0 {
logger.WithFields(logrus.Fields{
"count": count,
}).Info("returned jobs to queues")
}
2021-07-12 19:36:22 +00:00
}
func reportStats(ctx context.Context, logger *logrus.Logger, statsd *statsd.Client, pool *pgxpool.Pool) {
var (
count int64
metrics = []struct {
query string
name string
}{
{"SELECT COUNT(*) FROM accounts", "apollo.registrations.accounts"},
{"SELECT COUNT(*) FROM devices", "apollo.registrations.devices"},
2021-10-17 14:17:41 +00:00
{"SELECT COUNT(*) FROM subreddits", "apollo.registrations.subreddits"},
{"SELECT COUNT(*) FROM users", "apollo.registrations.users"},
}
)
for _, metric := range metrics {
2021-09-25 13:19:42 +00:00
_ = pool.QueryRow(ctx, metric.query).Scan(&count)
_ = statsd.Gauge(metric.name, float64(count), []string{}, 1)
logger.WithFields(logrus.Fields{
"count": count,
"metric": metric.name,
}).Debug("fetched metrics")
}
}
2021-10-09 14:59:20 +00:00
func enqueueUsers(ctx context.Context, logger *logrus.Logger, statsd *statsd.Client, pool *pgxpool.Pool, queue rmq.Queue) {
now := time.Now()
2022-03-28 21:05:01 +00:00
next := now.Add(domain.NotificationCheckInterval)
2021-10-09 14:59:20 +00:00
ids := []int64{}
2021-10-17 16:04:09 +00:00
defer func() {
tags := []string{"queue:users"}
_ = statsd.Histogram("apollo.queue.enqueued", float64(len(ids)), tags, 1)
_ = statsd.Histogram("apollo.queue.runtime", float64(time.Since(now).Milliseconds()), tags, 1)
}()
2021-10-09 14:59:20 +00:00
err := pool.BeginFunc(ctx, func(tx pgx.Tx) error {
stmt := `
2022-05-21 21:15:24 +00:00
UPDATE users
SET next_check_at = $2
WHERE id IN (
SELECT id
2021-10-09 14:59:20 +00:00
FROM users
2022-03-28 21:05:01 +00:00
WHERE next_check_at < $1
ORDER BY next_check_at
2022-05-21 21:15:24 +00:00
FOR UPDATE SKIP LOCKED
2021-10-09 14:59:20 +00:00
LIMIT 100
)
RETURNING users.id`
2022-03-28 21:05:01 +00:00
rows, err := tx.Query(ctx, stmt, now, next)
2021-10-09 14:59:20 +00:00
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id int64
_ = rows.Scan(&id)
ids = append(ids, id)
}
return nil
})
if err != nil {
logger.WithFields(logrus.Fields{
"err": err,
}).Error("failed to fetch batch of users")
return
}
if len(ids) == 0 {
return
}
logger.WithFields(logrus.Fields{
"count": len(ids),
2022-03-28 21:05:01 +00:00
"start": now,
2021-10-09 14:59:20 +00:00
}).Debug("enqueueing user batch")
batchIds := make([]string, len(ids))
for i, id := range ids {
batchIds[i] = strconv.FormatInt(id, 10)
}
if err = queue.Publish(batchIds...); err != nil {
logger.WithFields(logrus.Fields{
"err": err,
}).Error("failed to enqueue user")
}
}
2021-10-10 15:51:42 +00:00
func enqueueSubreddits(ctx context.Context, logger *logrus.Logger, statsd *statsd.Client, pool *pgxpool.Pool, queues []rmq.Queue) {
2021-09-25 16:56:01 +00:00
now := time.Now()
2022-03-28 21:05:01 +00:00
next := now.Add(domain.SubredditCheckInterval)
2021-09-25 16:56:01 +00:00
ids := []int64{}
2021-10-17 16:04:09 +00:00
defer func() {
tags := []string{"queue:subreddits"}
_ = statsd.Histogram("apollo.queue.enqueued", float64(len(ids)), tags, 1)
_ = statsd.Histogram("apollo.queue.runtime", float64(time.Since(now).Milliseconds()), tags, 1)
}()
2021-09-25 16:56:01 +00:00
err := pool.BeginFunc(ctx, func(tx pgx.Tx) error {
stmt := `
2022-05-21 21:15:24 +00:00
UPDATE subreddits
SET next_check_at = $2
WHERE subreddits.id IN(
SELECT id
2021-09-25 16:56:01 +00:00
FROM subreddits
2022-03-28 21:05:01 +00:00
WHERE next_check_at < $1
ORDER BY next_check_at
2022-05-21 21:15:24 +00:00
FOR UPDATE SKIP LOCKED
2021-09-25 16:56:01 +00:00
LIMIT 100
)
RETURNING subreddits.id`
2022-03-28 21:05:01 +00:00
rows, err := tx.Query(ctx, stmt, now, next)
2021-09-25 16:56:01 +00:00
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id int64
_ = rows.Scan(&id)
ids = append(ids, id)
}
return nil
})
if err != nil {
logger.WithFields(logrus.Fields{
"err": err,
}).Error("failed to fetch batch of subreddits")
return
}
if len(ids) == 0 {
return
}
logger.WithFields(logrus.Fields{
"count": len(ids),
2022-03-28 21:05:01 +00:00
"start": now,
2021-09-25 16:56:01 +00:00
}).Debug("enqueueing subreddit batch")
batchIds := make([]string, len(ids))
for i, id := range ids {
batchIds[i] = strconv.FormatInt(id, 10)
}
2021-10-10 15:51:42 +00:00
for _, queue := range queues {
if err = queue.Publish(batchIds...); err != nil {
logger.WithFields(logrus.Fields{
"queue": queue,
"err": err,
}).Error("failed to enqueue subreddit")
}
2021-09-25 16:56:01 +00:00
}
}
2021-10-17 14:17:41 +00:00
func enqueueStuckAccounts(ctx context.Context, logger *logrus.Logger, statsd *statsd.Client, pool *pgxpool.Pool, queue rmq.Queue) {
now := time.Now()
2022-03-28 21:05:01 +00:00
next := now.Add(domain.StuckNotificationCheckInterval)
2021-10-17 14:17:41 +00:00
ids := []int64{}
2021-10-17 16:04:09 +00:00
defer func() {
tags := []string{"queue:stuck-accounts"}
_ = statsd.Histogram("apollo.queue.enqueued", float64(len(ids)), tags, 1)
_ = statsd.Histogram("apollo.queue.runtime", float64(time.Since(now).Milliseconds()), tags, 1)
}()
2021-10-17 14:17:41 +00:00
err := pool.BeginFunc(ctx, func(tx pgx.Tx) error {
stmt := `
2022-05-21 21:15:24 +00:00
UPDATE accounts
SET next_stuck_notification_check_at = $2
WHERE accounts.id IN(
SELECT id
2021-10-17 14:17:41 +00:00
FROM accounts
2022-05-21 21:15:24 +00:00
WHERE next_stuck_notification_check_at < $1
2022-03-28 21:05:01 +00:00
ORDER BY next_stuck_notification_check_at
2022-05-21 21:15:24 +00:00
FOR UPDATE SKIP LOCKED
2021-10-17 14:17:41 +00:00
LIMIT 500
)
RETURNING accounts.id`
2022-03-28 21:05:01 +00:00
rows, err := tx.Query(ctx, stmt, now, next)
2021-10-17 14:17:41 +00:00
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id int64
_ = rows.Scan(&id)
ids = append(ids, id)
}
return nil
})
if err != nil {
logger.WithFields(logrus.Fields{
"err": err,
}).Error("failed to fetch possible stuck accounts")
return
}
if len(ids) == 0 {
return
}
logger.WithFields(logrus.Fields{
"count": len(ids),
2022-03-28 21:05:01 +00:00
"start": now,
2021-10-17 14:17:41 +00:00
}).Debug("enqueueing stuck account batch")
2021-07-09 02:15:28 +00:00
2021-10-17 14:17:41 +00:00
batchIds := make([]string, len(ids))
for i, id := range ids {
batchIds[i] = strconv.FormatInt(id, 10)
}
if err = queue.Publish(batchIds...); err != nil {
logger.WithFields(logrus.Fields{
"queue": queue,
"err": err,
}).Error("failed to enqueue stuck accounts")
}
}
func enqueueAccounts(ctx context.Context, logger *logrus.Logger, statsd *statsd.Client, pool *pgxpool.Pool, redisConn *redis.Client, luaSha string, queue rmq.Queue) {
now := time.Now()
2022-03-28 21:05:01 +00:00
next := now.Add(domain.NotificationCheckInterval)
2021-10-17 16:04:09 +00:00
ids := []int64{}
enqueued := 0
skipped := 0
defer func() {
tags := []string{"queue:notifications"}
_ = statsd.Histogram("apollo.queue.enqueued", float64(enqueued), tags, 1)
_ = statsd.Histogram("apollo.queue.skipped", float64(skipped), tags, 1)
_ = statsd.Histogram("apollo.queue.runtime", float64(time.Since(now).Milliseconds()), tags, 1)
}()
2021-07-09 01:07:01 +00:00
2021-07-08 23:03:46 +00:00
err := pool.BeginFunc(ctx, func(tx pgx.Tx) error {
stmt := `
2022-05-21 21:15:24 +00:00
UPDATE accounts
SET next_notification_check_at = $2
WHERE accounts.id IN(
SELECT id
2021-07-08 23:03:46 +00:00
FROM accounts
2022-05-21 21:15:24 +00:00
WHERE next_notification_check_at < $1
2022-03-28 21:05:01 +00:00
ORDER BY next_notification_check_at
2022-05-21 21:15:24 +00:00
FOR UPDATE SKIP LOCKED
2022-05-22 23:35:03 +00:00
LIMIT 5000
2021-07-08 23:03:46 +00:00
)
RETURNING accounts.id`
2022-03-28 21:05:01 +00:00
rows, err := tx.Query(ctx, stmt, now, next)
2021-07-08 23:03:46 +00:00
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id int64
2021-09-25 13:19:42 +00:00
_ = rows.Scan(&id)
2021-07-08 23:03:46 +00:00
ids = append(ids, id)
}
return nil
})
if err != nil {
logger.WithFields(logrus.Fields{
"err": err,
}).Error("failed to fetch batch of accounts")
return
}
2021-10-17 14:17:41 +00:00
if len(ids) == 0 {
return
}
2021-07-08 23:03:46 +00:00
logger.WithFields(logrus.Fields{
"count": len(ids),
2022-03-28 21:05:01 +00:00
"start": now,
2021-07-08 23:03:46 +00:00
}).Debug("enqueueing account batch")
2021-07-09 03:12:50 +00:00
// Split ids in batches
for i := 0; i < len(ids); i += batchSize {
j := i + batchSize
if j > len(ids) {
j = len(ids)
2021-07-08 23:03:46 +00:00
}
2021-07-09 03:12:50 +00:00
batch := Int64Slice(ids[i:j])
logger.WithFields(logrus.Fields{
"len": len(batch),
}).Debug("enqueueing batch")
res, err := redisConn.EvalSha(ctx, luaSha, []string{"locks:accounts"}, batch).Result()
2021-07-09 03:12:50 +00:00
if err != nil {
2021-07-09 00:26:01 +00:00
logger.WithFields(logrus.Fields{
2021-07-09 03:12:50 +00:00
"err": err,
}).Error("failed to check for locked accounts")
2021-07-09 00:26:01 +00:00
}
2021-07-09 03:12:50 +00:00
vals := res.([]interface{})
skipped += len(batch) - len(vals)
enqueued += len(vals)
2021-07-09 03:12:50 +00:00
if len(vals) == 0 {
continue
}
batchIds := make([]string, len(vals))
for k, v := range vals {
batchIds[k] = strconv.FormatInt(v.(int64), 10)
}
if err = queue.Publish(batchIds...); err != nil {
logger.WithFields(logrus.Fields{
"err": err,
}).Error("failed to enqueue account")
}
2021-07-08 23:03:46 +00:00
}
logger.WithFields(logrus.Fields{
2021-07-09 00:26:01 +00:00
"count": enqueued,
"skipped": skipped,
2022-03-28 21:05:01 +00:00
"start": now,
2021-07-09 06:00:57 +00:00
}).Debug("done enqueueing account batch")
2021-07-08 23:03:46 +00:00
}
2021-07-09 03:12:50 +00:00
type Int64Slice []int64
func (ii Int64Slice) MarshalBinary() (data []byte, err error) {
bytes, err := json.Marshal(ii)
return bytes, err
}