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")
+	}
+}