Mercurial > lbo > hg > goe_bot
changeset 43:ad3a8f150464
Implement reminder background thread and storage interface
author | Lewin Bormann <lbo@spheniscida.de> |
---|---|
date | Sat, 10 Dec 2016 16:06:29 +0100 |
parents | a9858e4be3d6 |
children | 88245ca6ed0e |
files | remind.go sql/remind.go |
diffstat | 2 files changed, 208 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/remind.go Sat Dec 10 16:06:29 2016 +0100 @@ -0,0 +1,91 @@ +// A background routine keeps polling the database for due reminders. +// This is slightly inefficient but easier for now. +package main + +import ( + "log" + "time" + + "bitbucket.org/dermesser/goe_bot/sql" +) + +const ( + pollTime = 2 * time.Second + maxAttempts = 3 +) + +type reminder sql.Reminder + +func (rm reminder) fire() error { + // Fired, but not yet removed + if rm.Fired { + return rm.removeFromDB() + } + + msg := sendMessage{Chat_ID: rm.ChatID, Parse_Mode: "Markdown", + Reply_To_Message_Id: rm.ReplyTo, Text: rm.Text} + + rm.Attempts++ + err := sendChatMessage(msg) + + if err != nil { + log.Println("Couldn't send reminder", msg, ":", err) + } else { + rm.Fired = true + return rm.removeFromDB() + } + + return err +} + +func (rm reminder) removeFromDB() error { + reminders, err := backend.Reminders() + + if err != nil { + return err + } + + return reminders.MarkRemindersDone([]uint{rm.ReminderID}) +} + +type reminders struct { +} + +// This runs in a goroutine and checks for expired reminders (every 2 seconds, for now) +func (r *reminders) reminderChecker() { + for { + db, err := backend.Reminders() + + if err != nil { + log.Println("Couldn't get Reminders object:", err) + time.Sleep(10 * time.Second) + continue + } + + inner: + for { + time.Sleep(2 * time.Second) + now := time.Now() + + events, err := db.SelectDueReminders() + + if err != nil { + log.Println("Couldn't fetch reminders:", err) + time.Sleep(10 * time.Second) + continue inner + } + + for _, rm := range events { + if !now.After(rm.Due) { + continue + } + + err := reminder(rm).fire() + + if err != nil { + // We keep trying to fire an event if it hasn't fired successfully + } + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sql/remind.go Sat Dec 10 16:06:29 2016 +0100 @@ -0,0 +1,117 @@ +// An access interface for storing and retrieving quotes. +package sql + +import ( + "errors" + "log" + "time" +) + +// prepared statements +const ( + // Parameters: 1 = due time, 2 = text, 3 = chat ID, 4 = command message ID + // Returns: id INTEGER + insertReminder = `INSERT INTO reminders (created, due, description, chat_id, orig_msg_id) + VALUES (now(), $1, $2, $3, $4) RETURNING id` + // Parameters: n/a + // Returns: id INTEGER, due TIMESTAMP, owner TEXT, description TEXT, chat_id INTEGER, orig_msg_id INTEGER + selectDueReminders = `SELECT id, due, owner, description, chat_id, orig_msg_id FROM reminders WHERE NOT done AND now() > due ORDER BY due ASC` + // Parameters: 1 = reminder ID + markReminderDone = `UPDATE reminders SET done = true WHERE id = $1` +) + +type Reminder struct { + ReminderID uint + Due time.Time + Owner string + Text string + ChatID int64 + ReplyTo int64 + + // Used by the reminder loop + Attempts int + Fired bool +} + +type Reminders struct { + db *Storage +} + +func newReminders(s *Storage) (Reminders, error) { + r := Reminders{db: s} + return r, r.prewarm() +} + +func (r Reminders) prewarm() error { + return r.db.prewarm([]string{ + insertReminder, + selectDueReminders, + markReminderDone}) +} + +func (r Reminders) Remove(reminderID uint) error { + if stmt, ok := r.db.prepared[markReminderDone]; ok { + _, err := stmt.Exec(reminderID) + + if err != nil { + log.Println("Couldn't mark reminder done:", err) + } + + return err + } else { + log.Println("Couldn't find prepared statement for", markReminderDone) + return errors.New("couldn't find prepared statement") + } +} + +func (r Reminders) SelectDueReminders() ([]Reminder, error) { + if stmt, ok := r.db.prepared[selectDueReminders]; ok { + rows, err := stmt.Query() + + if err != nil { + log.Println("Couldn't query due reminders") + return nil, err + } + + defer rows.Close() + + rm := make([]Reminder, 0, 16) + + for rows.Next() { + reminder := Reminder{} + err := rows.Scan(&reminder.ReminderID, &reminder.Due, + &reminder.Owner, &reminder.Text, &reminder.ChatID, &reminder.ReplyTo) + + if err != nil { + log.Println("Couldn't scan reminder row:", err) + return nil, err + } + + rm = append(rm, reminder) + } + + return rm, nil + } else { + log.Println("Couldn't find prepared statement for", selectDueReminders) + return nil, errors.New("couldn't find prepared statement") + } +} + +// If this returns an error, it can be called with the same IDs again. +func (r Reminders) MarkRemindersDone(ids []uint) error { + if stmt, ok := r.db.prepared[markReminderDone]; ok { + for _, id := range ids { + _, err := stmt.Exec(id) + + if err != nil { + log.Println("Couldn't mark reminder", id, "as done:", err) + return err + } + } + + return nil + } else { + log.Println("Couldn't find prepared statement for", markReminderDone) + return errors.New("couldn't find prepared statement") + } +}