WIP_ Add 'At:'/'Delay:' headers to support scheduled messages
This commit is contained in:
parent
aacdda94e1
commit
196c86d12b
8 changed files with 311 additions and 112 deletions
|
@ -11,6 +11,7 @@ const (
|
||||||
DefaultCacheDuration = 12 * time.Hour
|
DefaultCacheDuration = 12 * time.Hour
|
||||||
DefaultKeepaliveInterval = 30 * time.Second
|
DefaultKeepaliveInterval = 30 * time.Second
|
||||||
DefaultManagerInterval = time.Minute
|
DefaultManagerInterval = time.Minute
|
||||||
|
DefaultAtSenderInterval = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines all the limits
|
// Defines all the limits
|
||||||
|
@ -35,6 +36,7 @@ type Config struct {
|
||||||
CacheDuration time.Duration
|
CacheDuration time.Duration
|
||||||
KeepaliveInterval time.Duration
|
KeepaliveInterval time.Duration
|
||||||
ManagerInterval time.Duration
|
ManagerInterval time.Duration
|
||||||
|
AtSenderInterval time.Duration
|
||||||
GlobalTopicLimit int
|
GlobalTopicLimit int
|
||||||
VisitorRequestLimitBurst int
|
VisitorRequestLimitBurst int
|
||||||
VisitorRequestLimitReplenish time.Duration
|
VisitorRequestLimitReplenish time.Duration
|
||||||
|
@ -54,6 +56,7 @@ func New(listenHTTP string) *Config {
|
||||||
CacheDuration: DefaultCacheDuration,
|
CacheDuration: DefaultCacheDuration,
|
||||||
KeepaliveInterval: DefaultKeepaliveInterval,
|
KeepaliveInterval: DefaultKeepaliveInterval,
|
||||||
ManagerInterval: DefaultManagerInterval,
|
ManagerInterval: DefaultManagerInterval,
|
||||||
|
AtSenderInterval: DefaultAtSenderInterval,
|
||||||
GlobalTopicLimit: DefaultGlobalTopicLimit,
|
GlobalTopicLimit: DefaultGlobalTopicLimit,
|
||||||
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
||||||
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
||||||
|
|
|
@ -14,8 +14,10 @@ var (
|
||||||
// i.e. message structs with the Event messageEvent.
|
// i.e. message structs with the Event messageEvent.
|
||||||
type cache interface {
|
type cache interface {
|
||||||
AddMessage(m *message) error
|
AddMessage(m *message) error
|
||||||
Messages(topic string, since sinceTime) ([]*message, error)
|
Messages(topic string, since sinceTime, scheduled bool) ([]*message, error)
|
||||||
|
MessagesDue() ([]*message, error)
|
||||||
MessageCount(topic string) (int, error)
|
MessageCount(topic string) (int, error)
|
||||||
Topics() (map[string]*topic, error)
|
Topics() (map[string]*topic, error)
|
||||||
Prune(olderThan time.Time) error
|
Prune(olderThan time.Time) error
|
||||||
|
MarkPublished(m *message) error
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type memCache struct {
|
type memCache struct {
|
||||||
messages map[string][]*message
|
messages map[string][]*message
|
||||||
nop bool
|
scheduled map[string]*message // Message ID -> message
|
||||||
mu sync.Mutex
|
nop bool
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ cache = (*memCache)(nil)
|
var _ cache = (*memCache)(nil)
|
||||||
|
@ -16,8 +18,9 @@ var _ cache = (*memCache)(nil)
|
||||||
// newMemCache creates an in-memory cache
|
// newMemCache creates an in-memory cache
|
||||||
func newMemCache() *memCache {
|
func newMemCache() *memCache {
|
||||||
return &memCache{
|
return &memCache{
|
||||||
messages: make(map[string][]*message),
|
messages: make(map[string][]*message),
|
||||||
nop: false,
|
scheduled: make(map[string]*message),
|
||||||
|
nop: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,77 +28,109 @@ func newMemCache() *memCache {
|
||||||
// it is always empty and can be used if caching is entirely disabled
|
// it is always empty and can be used if caching is entirely disabled
|
||||||
func newNopCache() *memCache {
|
func newNopCache() *memCache {
|
||||||
return &memCache{
|
return &memCache{
|
||||||
messages: make(map[string][]*message),
|
messages: make(map[string][]*message),
|
||||||
nop: true,
|
scheduled: make(map[string]*message),
|
||||||
|
nop: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *memCache) AddMessage(m *message) error {
|
func (c *memCache) AddMessage(m *message) error {
|
||||||
s.mu.Lock()
|
c.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
if s.nop {
|
if c.nop {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if m.Event != messageEvent {
|
if m.Event != messageEvent {
|
||||||
return errUnexpectedMessageType
|
return errUnexpectedMessageType
|
||||||
}
|
}
|
||||||
if _, ok := s.messages[m.Topic]; !ok {
|
if _, ok := c.messages[m.Topic]; !ok {
|
||||||
s.messages[m.Topic] = make([]*message, 0)
|
c.messages[m.Topic] = make([]*message, 0)
|
||||||
}
|
}
|
||||||
s.messages[m.Topic] = append(s.messages[m.Topic], m)
|
delayed := m.Time > time.Now().Unix()
|
||||||
|
if delayed {
|
||||||
|
c.scheduled[m.ID] = m
|
||||||
|
}
|
||||||
|
c.messages[m.Topic] = append(c.messages[m.Topic], m)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *memCache) Messages(topic string, since sinceTime) ([]*message, error) {
|
func (c *memCache) Messages(topic string, since sinceTime, scheduled bool) ([]*message, error) {
|
||||||
s.mu.Lock()
|
c.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
if _, ok := s.messages[topic]; !ok || since.IsNone() {
|
if _, ok := c.messages[topic]; !ok || since.IsNone() {
|
||||||
return make([]*message, 0), nil
|
return make([]*message, 0), nil
|
||||||
}
|
}
|
||||||
messages := make([]*message, 0) // copy!
|
messages := make([]*message, 0)
|
||||||
for _, m := range s.messages[topic] {
|
for _, m := range c.messages[topic] {
|
||||||
msgTime := time.Unix(m.Time, 0)
|
_, messageScheduled := c.scheduled[m.ID]
|
||||||
if msgTime == since.Time() || msgTime.After(since.Time()) {
|
include := m.Time >= since.Time().Unix() && (!messageScheduled || scheduled)
|
||||||
|
if include {
|
||||||
messages = append(messages, m)
|
messages = append(messages, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sort.Slice(messages, func(i, j int) bool {
|
||||||
|
return messages[i].Time < messages[j].Time
|
||||||
|
})
|
||||||
return messages, nil
|
return messages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *memCache) MessageCount(topic string) (int, error) {
|
func (c *memCache) MessagesDue() ([]*message, error) {
|
||||||
s.mu.Lock()
|
c.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
if _, ok := s.messages[topic]; !ok {
|
messages := make([]*message, 0)
|
||||||
return 0, nil
|
for _, m := range c.scheduled {
|
||||||
|
due := time.Now().Unix() >= m.Time
|
||||||
|
if due {
|
||||||
|
messages = append(messages, m)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return len(s.messages[topic]), nil
|
sort.Slice(messages, func(i, j int) bool {
|
||||||
|
return messages[i].Time < messages[j].Time
|
||||||
|
})
|
||||||
|
return messages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *memCache) Topics() (map[string]*topic, error) {
|
func (c *memCache) MarkPublished(m *message) error {
|
||||||
s.mu.Lock()
|
c.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
delete(c.scheduled, m.ID)
|
||||||
|
c.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) MessageCount(topic string) (int, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if _, ok := c.messages[topic]; !ok {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return len(c.messages[topic]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) Topics() (map[string]*topic, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
topics := make(map[string]*topic)
|
topics := make(map[string]*topic)
|
||||||
for topic := range s.messages {
|
for topic := range c.messages {
|
||||||
topics[topic] = newTopic(topic)
|
topics[topic] = newTopic(topic)
|
||||||
}
|
}
|
||||||
return topics, nil
|
return topics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *memCache) Prune(olderThan time.Time) error {
|
func (c *memCache) Prune(olderThan time.Time) error {
|
||||||
s.mu.Lock()
|
c.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
for topic := range s.messages {
|
for topic := range c.messages {
|
||||||
s.pruneTopic(topic, olderThan)
|
c.pruneTopic(topic, olderThan)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *memCache) pruneTopic(topic string, olderThan time.Time) {
|
func (c *memCache) pruneTopic(topic string, olderThan time.Time) {
|
||||||
messages := make([]*message, 0)
|
messages := make([]*message, 0)
|
||||||
for _, m := range s.messages[topic] {
|
for _, m := range c.messages[topic] {
|
||||||
if m.Time >= olderThan.Unix() {
|
if m.Time >= olderThan.Unix() {
|
||||||
messages = append(messages, m)
|
messages = append(messages, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.messages[topic] = messages
|
c.messages[topic] = messages
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,10 @@ func TestMemCache_Messages(t *testing.T) {
|
||||||
testCacheMessages(t, newMemCache())
|
testCacheMessages(t, newMemCache())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMemCache_MessagesScheduled(t *testing.T) {
|
||||||
|
testCacheMessagesScheduled(t, newMemCache())
|
||||||
|
}
|
||||||
|
|
||||||
func TestMemCache_Topics(t *testing.T) {
|
func TestMemCache_Topics(t *testing.T) {
|
||||||
testCacheTopics(t, newMemCache())
|
testCacheTopics(t, newMemCache())
|
||||||
}
|
}
|
||||||
|
@ -25,7 +29,7 @@ func TestMemCache_NopCache(t *testing.T) {
|
||||||
c := newNopCache()
|
c := newNopCache()
|
||||||
assert.Nil(t, c.AddMessage(newDefaultMessage("mytopic", "my message")))
|
assert.Nil(t, c.AddMessage(newDefaultMessage("mytopic", "my message")))
|
||||||
|
|
||||||
messages, err := c.Messages("mytopic", sinceAllMessages)
|
messages, err := c.Messages("mytopic", sinceAllMessages, false)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Empty(t, messages)
|
assert.Empty(t, messages)
|
||||||
|
|
||||||
|
|
|
@ -21,19 +21,32 @@ const (
|
||||||
message VARCHAR(512) NOT NULL,
|
message VARCHAR(512) NOT NULL,
|
||||||
title VARCHAR(256) NOT NULL,
|
title VARCHAR(256) NOT NULL,
|
||||||
priority INT NOT NULL,
|
priority INT NOT NULL,
|
||||||
tags VARCHAR(256) NOT NULL
|
tags VARCHAR(256) NOT NULL,
|
||||||
|
published INT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
|
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
|
||||||
COMMIT;
|
COMMIT;
|
||||||
`
|
`
|
||||||
insertMessageQuery = `INSERT INTO messages (id, time, topic, message, title, priority, tags) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
insertMessageQuery = `INSERT INTO messages (id, time, topic, message, title, priority, tags, published) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
pruneMessagesQuery = `DELETE FROM messages WHERE time < ?`
|
pruneMessagesQuery = `DELETE FROM messages WHERE time < ?`
|
||||||
selectMessagesSinceTimeQuery = `
|
selectMessagesSinceTimeQuery = `
|
||||||
SELECT id, time, message, title, priority, tags
|
SELECT id, time, topic, message, title, priority, tags
|
||||||
|
FROM messages
|
||||||
|
WHERE topic = ? AND time >= ? AND published = 1
|
||||||
|
ORDER BY time ASC
|
||||||
|
`
|
||||||
|
selectMessagesSinceTimeIncludeScheduledQuery = `
|
||||||
|
SELECT id, time, topic, message, title, priority, tags
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE topic = ? AND time >= ?
|
WHERE topic = ? AND time >= ?
|
||||||
ORDER BY time ASC
|
ORDER BY time ASC
|
||||||
`
|
`
|
||||||
|
selectMessagesDueQuery = `
|
||||||
|
SELECT id, time, topic, message, title, priority, tags
|
||||||
|
FROM messages
|
||||||
|
WHERE time <= ? AND published = 0
|
||||||
|
`
|
||||||
|
updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE id = ?`
|
||||||
selectMessagesCountQuery = `SELECT COUNT(*) FROM messages`
|
selectMessagesCountQuery = `SELECT COUNT(*) FROM messages`
|
||||||
selectMessageCountForTopicQuery = `SELECT COUNT(*) FROM messages WHERE topic = ?`
|
selectMessageCountForTopicQuery = `SELECT COUNT(*) FROM messages WHERE topic = ?`
|
||||||
selectTopicsQuery = `SELECT topic FROM messages GROUP BY topic`
|
selectTopicsQuery = `SELECT topic FROM messages GROUP BY topic`
|
||||||
|
@ -41,7 +54,7 @@ const (
|
||||||
|
|
||||||
// Schema management queries
|
// Schema management queries
|
||||||
const (
|
const (
|
||||||
currentSchemaVersion = 1
|
currentSchemaVersion = 2
|
||||||
createSchemaVersionTableQuery = `
|
createSchemaVersionTableQuery = `
|
||||||
CREATE TABLE IF NOT EXISTS schemaVersion (
|
CREATE TABLE IF NOT EXISTS schemaVersion (
|
||||||
id INT PRIMARY KEY,
|
id INT PRIMARY KEY,
|
||||||
|
@ -49,6 +62,7 @@ const (
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
insertSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)`
|
insertSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)`
|
||||||
|
updateSchemaVersion = `UPDATE schemaVersion SET version = ? WHERE id = 1`
|
||||||
selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
|
selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1`
|
||||||
|
|
||||||
// 0 -> 1
|
// 0 -> 1
|
||||||
|
@ -59,6 +73,13 @@ const (
|
||||||
ALTER TABLE messages ADD COLUMN tags VARCHAR(256) NOT NULL DEFAULT('');
|
ALTER TABLE messages ADD COLUMN tags VARCHAR(256) NOT NULL DEFAULT('');
|
||||||
COMMIT;
|
COMMIT;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// 1 -> 2
|
||||||
|
migrate1To2AlterMessagesTableQuery = `
|
||||||
|
BEGIN;
|
||||||
|
ALTER TABLE messages ADD COLUMN published INT NOT NULL DEFAULT(1);
|
||||||
|
COMMIT;
|
||||||
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
type sqliteCache struct {
|
type sqliteCache struct {
|
||||||
|
@ -84,46 +105,39 @@ func (c *sqliteCache) AddMessage(m *message) error {
|
||||||
if m.Event != messageEvent {
|
if m.Event != messageEvent {
|
||||||
return errUnexpectedMessageType
|
return errUnexpectedMessageType
|
||||||
}
|
}
|
||||||
_, err := c.db.Exec(insertMessageQuery, m.ID, m.Time, m.Topic, m.Message, m.Title, m.Priority, strings.Join(m.Tags, ","))
|
published := m.Time <= time.Now().Unix()
|
||||||
|
_, err := c.db.Exec(insertMessageQuery, m.ID, m.Time, m.Topic, m.Message, m.Title, m.Priority, strings.Join(m.Tags, ","), published)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *sqliteCache) Messages(topic string, since sinceTime) ([]*message, error) {
|
func (c *sqliteCache) Messages(topic string, since sinceTime, scheduled bool) ([]*message, error) {
|
||||||
if since.IsNone() {
|
if since.IsNone() {
|
||||||
return make([]*message, 0), nil
|
return make([]*message, 0), nil
|
||||||
}
|
}
|
||||||
rows, err := c.db.Query(selectMessagesSinceTimeQuery, topic, since.Time().Unix())
|
var rows *sql.Rows
|
||||||
|
var err error
|
||||||
|
if scheduled {
|
||||||
|
rows, err = c.db.Query(selectMessagesSinceTimeIncludeScheduledQuery, topic, since.Time().Unix())
|
||||||
|
} else {
|
||||||
|
rows, err = c.db.Query(selectMessagesSinceTimeQuery, topic, since.Time().Unix())
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
return readMessages(rows)
|
||||||
messages := make([]*message, 0)
|
}
|
||||||
for rows.Next() {
|
|
||||||
var timestamp int64
|
func (c *sqliteCache) MessagesDue() ([]*message, error) {
|
||||||
var priority int
|
rows, err := c.db.Query(selectMessagesDueQuery, time.Now().Unix())
|
||||||
var id, msg, title, tagsStr string
|
if err != nil {
|
||||||
if err := rows.Scan(&id, ×tamp, &msg, &title, &priority, &tagsStr); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var tags []string
|
|
||||||
if tagsStr != "" {
|
|
||||||
tags = strings.Split(tagsStr, ",")
|
|
||||||
}
|
|
||||||
messages = append(messages, &message{
|
|
||||||
ID: id,
|
|
||||||
Time: timestamp,
|
|
||||||
Event: messageEvent,
|
|
||||||
Topic: topic,
|
|
||||||
Message: msg,
|
|
||||||
Title: title,
|
|
||||||
Priority: priority,
|
|
||||||
Tags: tags,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return messages, nil
|
return readMessages(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *sqliteCache) MarkPublished(m *message) error {
|
||||||
|
_, err := c.db.Exec(updateMessagePublishedQuery, m.ID)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *sqliteCache) MessageCount(topic string) (int, error) {
|
func (c *sqliteCache) MessageCount(topic string) (int, error) {
|
||||||
|
@ -169,6 +183,37 @@ func (c *sqliteCache) Prune(olderThan time.Time) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||||
|
defer rows.Close()
|
||||||
|
messages := make([]*message, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var timestamp int64
|
||||||
|
var priority int
|
||||||
|
var id, topic, msg, title, tagsStr string
|
||||||
|
if err := rows.Scan(&id, ×tamp, &topic, &msg, &title, &priority, &tagsStr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var tags []string
|
||||||
|
if tagsStr != "" {
|
||||||
|
tags = strings.Split(tagsStr, ",")
|
||||||
|
}
|
||||||
|
messages = append(messages, &message{
|
||||||
|
ID: id,
|
||||||
|
Time: timestamp,
|
||||||
|
Event: messageEvent,
|
||||||
|
Topic: topic,
|
||||||
|
Message: msg,
|
||||||
|
Title: title,
|
||||||
|
Priority: priority,
|
||||||
|
Tags: tags,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return messages, nil
|
||||||
|
}
|
||||||
|
|
||||||
func setupDB(db *sql.DB) error {
|
func setupDB(db *sql.DB) error {
|
||||||
// If 'messages' table does not exist, this must be a new database
|
// If 'messages' table does not exist, this must be a new database
|
||||||
rowsMC, err := db.Query(selectMessagesCountQuery)
|
rowsMC, err := db.Query(selectMessagesCountQuery)
|
||||||
|
@ -194,7 +239,9 @@ func setupDB(db *sql.DB) error {
|
||||||
if schemaVersion == currentSchemaVersion {
|
if schemaVersion == currentSchemaVersion {
|
||||||
return nil
|
return nil
|
||||||
} else if schemaVersion == 0 {
|
} else if schemaVersion == 0 {
|
||||||
return migrateFrom0To1(db)
|
return migrateFrom0(db)
|
||||||
|
} else if schemaVersion == 1 {
|
||||||
|
return migrateFrom1(db)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
|
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
|
||||||
}
|
}
|
||||||
|
@ -212,7 +259,7 @@ func setupNewDB(db *sql.DB) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateFrom0To1(db *sql.DB) error {
|
func migrateFrom0(db *sql.DB) error {
|
||||||
log.Print("Migrating cache database schema: from 0 to 1")
|
log.Print("Migrating cache database schema: from 0 to 1")
|
||||||
if _, err := db.Exec(migrate0To1AlterMessagesTableQuery); err != nil {
|
if _, err := db.Exec(migrate0To1AlterMessagesTableQuery); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -223,5 +270,16 @@ func migrateFrom0To1(db *sql.DB) error {
|
||||||
if _, err := db.Exec(insertSchemaVersion, 1); err != nil {
|
if _, err := db.Exec(insertSchemaVersion, 1); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return migrateFrom1(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateFrom1(db *sql.DB) error {
|
||||||
|
log.Print("Migrating cache database schema: from 1 to 2")
|
||||||
|
if _, err := db.Exec(migrate1To2AlterMessagesTableQuery); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(updateSchemaVersion, 2); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil // Update this when a new version is added
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSqliteCache_AddMessage(t *testing.T) {
|
func TestSqliteCache_Messages(t *testing.T) {
|
||||||
testCacheMessages(t, newSqliteTestCache(t))
|
testCacheMessages(t, newSqliteTestCache(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSqliteCache_MessagesScheduled(t *testing.T) {
|
||||||
|
testCacheMessagesScheduled(t, newSqliteTestCache(t))
|
||||||
|
}
|
||||||
|
|
||||||
func TestSqliteCache_Topics(t *testing.T) {
|
func TestSqliteCache_Topics(t *testing.T) {
|
||||||
testCacheTopics(t, newSqliteTestCache(t))
|
testCacheTopics(t, newSqliteTestCache(t))
|
||||||
}
|
}
|
||||||
|
@ -25,7 +29,7 @@ func TestSqliteCache_Prune(t *testing.T) {
|
||||||
testCachePrune(t, newSqliteTestCache(t))
|
testCachePrune(t, newSqliteTestCache(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSqliteCache_Migration_0to1(t *testing.T) {
|
func TestSqliteCache_Migration_From0(t *testing.T) {
|
||||||
filename := newSqliteTestCacheFile(t)
|
filename := newSqliteTestCacheFile(t)
|
||||||
db, err := sql.Open("sqlite3", filename)
|
db, err := sql.Open("sqlite3", filename)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
@ -53,7 +57,7 @@ func TestSqliteCache_Migration_0to1(t *testing.T) {
|
||||||
|
|
||||||
// Create cache to trigger migration
|
// Create cache to trigger migration
|
||||||
c := newSqliteTestCacheFromFile(t, filename)
|
c := newSqliteTestCacheFromFile(t, filename)
|
||||||
messages, err := c.Messages("mytopic", sinceAllMessages)
|
messages, err := c.Messages("mytopic", sinceAllMessages, false)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, 10, len(messages))
|
assert.Equal(t, 10, len(messages))
|
||||||
assert.Equal(t, "some message 5", messages[5].Message)
|
assert.Equal(t, "some message 5", messages[5].Message)
|
||||||
|
@ -67,7 +71,7 @@ func TestSqliteCache_Migration_0to1(t *testing.T) {
|
||||||
|
|
||||||
var schemaVersion int
|
var schemaVersion int
|
||||||
assert.Nil(t, rows.Scan(&schemaVersion))
|
assert.Nil(t, rows.Scan(&schemaVersion))
|
||||||
assert.Equal(t, 1, schemaVersion)
|
assert.Equal(t, 2, schemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSqliteTestCache(t *testing.T) *sqliteCache {
|
func newSqliteTestCache(t *testing.T) *sqliteCache {
|
||||||
|
|
|
@ -27,7 +27,7 @@ func testCacheMessages(t *testing.T, c cache) {
|
||||||
assert.Equal(t, 2, count)
|
assert.Equal(t, 2, count)
|
||||||
|
|
||||||
// mytopic: since all
|
// mytopic: since all
|
||||||
messages, _ := c.Messages("mytopic", sinceAllMessages)
|
messages, _ := c.Messages("mytopic", sinceAllMessages, false)
|
||||||
assert.Equal(t, 2, len(messages))
|
assert.Equal(t, 2, len(messages))
|
||||||
assert.Equal(t, "my message", messages[0].Message)
|
assert.Equal(t, "my message", messages[0].Message)
|
||||||
assert.Equal(t, "mytopic", messages[0].Topic)
|
assert.Equal(t, "mytopic", messages[0].Topic)
|
||||||
|
@ -38,11 +38,11 @@ func testCacheMessages(t *testing.T, c cache) {
|
||||||
assert.Equal(t, "my other message", messages[1].Message)
|
assert.Equal(t, "my other message", messages[1].Message)
|
||||||
|
|
||||||
// mytopic: since none
|
// mytopic: since none
|
||||||
messages, _ = c.Messages("mytopic", sinceNoMessages)
|
messages, _ = c.Messages("mytopic", sinceNoMessages, false)
|
||||||
assert.Empty(t, messages)
|
assert.Empty(t, messages)
|
||||||
|
|
||||||
// mytopic: since 2
|
// mytopic: since 2
|
||||||
messages, _ = c.Messages("mytopic", sinceTime(time.Unix(2, 0)))
|
messages, _ = c.Messages("mytopic", sinceTime(time.Unix(2, 0)), false)
|
||||||
assert.Equal(t, 1, len(messages))
|
assert.Equal(t, 1, len(messages))
|
||||||
assert.Equal(t, "my other message", messages[0].Message)
|
assert.Equal(t, "my other message", messages[0].Message)
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ func testCacheMessages(t *testing.T, c cache) {
|
||||||
assert.Equal(t, 1, count)
|
assert.Equal(t, 1, count)
|
||||||
|
|
||||||
// example: since all
|
// example: since all
|
||||||
messages, _ = c.Messages("example", sinceAllMessages)
|
messages, _ = c.Messages("example", sinceAllMessages, false)
|
||||||
assert.Equal(t, "my example message", messages[0].Message)
|
assert.Equal(t, "my example message", messages[0].Message)
|
||||||
|
|
||||||
// non-existing: count
|
// non-existing: count
|
||||||
|
@ -61,7 +61,7 @@ func testCacheMessages(t *testing.T, c cache) {
|
||||||
assert.Equal(t, 0, count)
|
assert.Equal(t, 0, count)
|
||||||
|
|
||||||
// non-existing: since all
|
// non-existing: since all
|
||||||
messages, _ = c.Messages("doesnotexist", sinceAllMessages)
|
messages, _ = c.Messages("doesnotexist", sinceAllMessages, false)
|
||||||
assert.Empty(t, messages)
|
assert.Empty(t, messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ func testCachePrune(t *testing.T, c cache) {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, 0, count)
|
assert.Equal(t, 0, count)
|
||||||
|
|
||||||
messages, err := c.Messages("mytopic", sinceAllMessages)
|
messages, err := c.Messages("mytopic", sinceAllMessages, false)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, 1, len(messages))
|
assert.Equal(t, 1, len(messages))
|
||||||
assert.Equal(t, "my other message", messages[0].Message)
|
assert.Equal(t, "my other message", messages[0].Message)
|
||||||
|
@ -116,8 +116,34 @@ func testCacheMessagesTagsPrioAndTitle(t *testing.T, c cache) {
|
||||||
m.Title = "some title"
|
m.Title = "some title"
|
||||||
assert.Nil(t, c.AddMessage(m))
|
assert.Nil(t, c.AddMessage(m))
|
||||||
|
|
||||||
messages, _ := c.Messages("mytopic", sinceAllMessages)
|
messages, _ := c.Messages("mytopic", sinceAllMessages, false)
|
||||||
assert.Equal(t, []string{"tag1", "tag2"}, messages[0].Tags)
|
assert.Equal(t, []string{"tag1", "tag2"}, messages[0].Tags)
|
||||||
assert.Equal(t, 5, messages[0].Priority)
|
assert.Equal(t, 5, messages[0].Priority)
|
||||||
assert.Equal(t, "some title", messages[0].Title)
|
assert.Equal(t, "some title", messages[0].Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCacheMessagesScheduled(t *testing.T, c cache) {
|
||||||
|
m1 := newDefaultMessage("mytopic", "message 1")
|
||||||
|
m2 := newDefaultMessage("mytopic", "message 2")
|
||||||
|
m2.Time = time.Now().Add(time.Hour).Unix()
|
||||||
|
m3 := newDefaultMessage("mytopic", "message 3")
|
||||||
|
m3.Time = time.Now().Add(time.Minute).Unix() // earlier than m2!
|
||||||
|
m4 := newDefaultMessage("mytopic2", "message 4")
|
||||||
|
m4.Time = time.Now().Add(time.Minute).Unix()
|
||||||
|
assert.Nil(t, c.AddMessage(m1))
|
||||||
|
assert.Nil(t, c.AddMessage(m2))
|
||||||
|
assert.Nil(t, c.AddMessage(m3))
|
||||||
|
|
||||||
|
messages, _ := c.Messages("mytopic", sinceAllMessages, false) // exclude scheduled
|
||||||
|
assert.Equal(t, 1, len(messages))
|
||||||
|
assert.Equal(t, "message 1", messages[0].Message)
|
||||||
|
|
||||||
|
messages, _ = c.Messages("mytopic", sinceAllMessages, true) // include scheduled
|
||||||
|
assert.Equal(t, 3, len(messages))
|
||||||
|
assert.Equal(t, "message 1", messages[0].Message)
|
||||||
|
assert.Equal(t, "message 3", messages[1].Message) // Order!
|
||||||
|
assert.Equal(t, "message 2", messages[2].Message)
|
||||||
|
|
||||||
|
messages, _ = c.MessagesDue()
|
||||||
|
assert.Empty(t, messages)
|
||||||
|
}
|
||||||
|
|
113
server/server.go
113
server/server.go
|
@ -73,6 +73,7 @@ var (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
messageLimit = 512
|
messageLimit = 512
|
||||||
|
minDelay = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -183,6 +184,15 @@ func (s *Server) Run() error {
|
||||||
s.updateStatsAndExpire()
|
s.updateStatsAndExpire()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(s.config.AtSenderInterval)
|
||||||
|
for {
|
||||||
|
<-ticker.C
|
||||||
|
if err := s.sendDelayedMessages(); err != nil {
|
||||||
|
log.Printf("error sending scheduled messages: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
listenStr := fmt.Sprintf("%s/http", s.config.ListenHTTP)
|
listenStr := fmt.Sprintf("%s/http", s.config.ListenHTTP)
|
||||||
if s.config.ListenHTTPS != "" {
|
if s.config.ListenHTTPS != "" {
|
||||||
listenStr += fmt.Sprintf(" %s/https", s.config.ListenHTTPS)
|
listenStr += fmt.Sprintf(" %s/https", s.config.ListenHTTPS)
|
||||||
|
@ -279,14 +289,17 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito
|
||||||
if m.Message == "" {
|
if m.Message == "" {
|
||||||
return errHTTPBadRequest
|
return errHTTPBadRequest
|
||||||
}
|
}
|
||||||
title, priority, tags, cache, firebase := parseHeaders(r.Header)
|
cache, firebase, err := parseHeaders(r.Header, m)
|
||||||
m.Title = title
|
if err != nil {
|
||||||
m.Priority = priority
|
|
||||||
m.Tags = tags
|
|
||||||
if err := t.Publish(m); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if s.firebase != nil && firebase {
|
delayed := m.Time > time.Now().Unix()
|
||||||
|
if !delayed {
|
||||||
|
if err := t.Publish(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.firebase != nil && firebase && !delayed {
|
||||||
go func() {
|
go func() {
|
||||||
if err := s.firebase(m); err != nil {
|
if err := s.firebase(m); err != nil {
|
||||||
log.Printf("Unable to publish to Firebase: %v", err.Error())
|
log.Printf("Unable to publish to Firebase: %v", err.Error())
|
||||||
|
@ -308,35 +321,62 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseHeaders(header http.Header) (title string, priority int, tags []string, cache bool, firebase bool) {
|
func parseHeaders(header http.Header, m *message) (cache bool, firebase bool, err error) {
|
||||||
title = readHeader(header, "x-title", "title", "ti", "t")
|
cache = readHeader(header, "x-cache", "cache") != "no"
|
||||||
|
firebase = readHeader(header, "x-firebase", "firebase") != "no"
|
||||||
|
m.Title = readHeader(header, "x-title", "title", "ti", "t")
|
||||||
priorityStr := readHeader(header, "x-priority", "priority", "prio", "p")
|
priorityStr := readHeader(header, "x-priority", "priority", "prio", "p")
|
||||||
if priorityStr != "" {
|
if priorityStr != "" {
|
||||||
switch strings.ToLower(priorityStr) {
|
switch strings.ToLower(priorityStr) {
|
||||||
case "1", "min":
|
case "1", "min":
|
||||||
priority = 1
|
m.Priority = 1
|
||||||
case "2", "low":
|
case "2", "low":
|
||||||
priority = 2
|
m.Priority = 2
|
||||||
case "3", "default":
|
case "3", "default":
|
||||||
priority = 3
|
m.Priority = 3
|
||||||
case "4", "high":
|
case "4", "high":
|
||||||
priority = 4
|
m.Priority = 4
|
||||||
case "5", "max", "urgent":
|
case "5", "max", "urgent":
|
||||||
priority = 5
|
m.Priority = 5
|
||||||
default:
|
default:
|
||||||
priority = 0
|
return false, false, errHTTPBadRequest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tagsStr := readHeader(header, "x-tags", "tag", "tags", "ta")
|
tagsStr := readHeader(header, "x-tags", "tag", "tags", "ta")
|
||||||
if tagsStr != "" {
|
if tagsStr != "" {
|
||||||
tags = make([]string, 0)
|
m.Tags = make([]string, 0)
|
||||||
for _, s := range strings.Split(tagsStr, ",") {
|
for _, s := range strings.Split(tagsStr, ",") {
|
||||||
tags = append(tags, strings.TrimSpace(s))
|
m.Tags = append(m.Tags, strings.TrimSpace(s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache = readHeader(header, "x-cache", "cache") != "no"
|
atStr := readHeader(header, "x-at", "at", "x-schedule", "schedule", "sched")
|
||||||
firebase = readHeader(header, "x-firebase", "firebase") != "no"
|
if atStr != "" {
|
||||||
return title, priority, tags, cache, firebase
|
if !cache {
|
||||||
|
return false, false, errHTTPBadRequest
|
||||||
|
}
|
||||||
|
at, err := strconv.Atoi(atStr)
|
||||||
|
if err != nil {
|
||||||
|
return false, false, errHTTPBadRequest
|
||||||
|
} else if int64(at) < time.Now().Add(minDelay).Unix() {
|
||||||
|
return false, false, errHTTPBadRequest
|
||||||
|
}
|
||||||
|
m.Time = int64(at)
|
||||||
|
} else {
|
||||||
|
delayStr := readHeader(header, "x-delay", "delay", "x-in", "in")
|
||||||
|
if delayStr != "" {
|
||||||
|
if !cache {
|
||||||
|
return false, false, errHTTPBadRequest
|
||||||
|
}
|
||||||
|
delay, err := time.ParseDuration(delayStr)
|
||||||
|
if err != nil {
|
||||||
|
return false, false, errHTTPBadRequest
|
||||||
|
} else if delay < minDelay {
|
||||||
|
return false, false, errHTTPBadRequest
|
||||||
|
}
|
||||||
|
m.Time = time.Now().Add(delay).Unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cache, firebase, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readHeader(header http.Header, names ...string) string {
|
func readHeader(header http.Header, names ...string) string {
|
||||||
|
@ -401,6 +441,7 @@ func (s *Server) handleSubscribe(w http.ResponseWriter, r *http.Request, v *visi
|
||||||
}
|
}
|
||||||
var wlock sync.Mutex
|
var wlock sync.Mutex
|
||||||
poll := r.URL.Query().Has("poll")
|
poll := r.URL.Query().Has("poll")
|
||||||
|
scheduled := r.URL.Query().Has("scheduled") || r.URL.Query().Has("sched")
|
||||||
sub := func(msg *message) error {
|
sub := func(msg *message) error {
|
||||||
wlock.Lock()
|
wlock.Lock()
|
||||||
defer wlock.Unlock()
|
defer wlock.Unlock()
|
||||||
|
@ -419,7 +460,7 @@ func (s *Server) handleSubscribe(w http.ResponseWriter, r *http.Request, v *visi
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
|
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
|
||||||
w.Header().Set("Content-Type", contentType+"; charset=utf-8") // Android/Volley client needs charset!
|
w.Header().Set("Content-Type", contentType+"; charset=utf-8") // Android/Volley client needs charset!
|
||||||
if poll {
|
if poll {
|
||||||
return s.sendOldMessages(topics, since, sub)
|
return s.sendOldMessages(topics, since, scheduled, sub)
|
||||||
}
|
}
|
||||||
subscriberIDs := make([]int, 0)
|
subscriberIDs := make([]int, 0)
|
||||||
for _, t := range topics {
|
for _, t := range topics {
|
||||||
|
@ -433,7 +474,7 @@ func (s *Server) handleSubscribe(w http.ResponseWriter, r *http.Request, v *visi
|
||||||
if err := sub(newOpenMessage(topicsStr)); err != nil { // Send out open message
|
if err := sub(newOpenMessage(topicsStr)); err != nil { // Send out open message
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.sendOldMessages(topics, since, sub); err != nil {
|
if err := s.sendOldMessages(topics, since, scheduled, sub); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
|
@ -449,12 +490,12 @@ func (s *Server) handleSubscribe(w http.ResponseWriter, r *http.Request, v *visi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) sendOldMessages(topics []*topic, since sinceTime, sub subscriber) error {
|
func (s *Server) sendOldMessages(topics []*topic, since sinceTime, scheduled bool, sub subscriber) error {
|
||||||
if since.IsNone() {
|
if since.IsNone() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, t := range topics {
|
for _, t := range topics {
|
||||||
messages, err := s.cache.Messages(t.ID, since)
|
messages, err := s.cache.Messages(t.ID, since, scheduled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -560,6 +601,32 @@ func (s *Server) updateStatsAndExpire() {
|
||||||
s.messages, len(s.topics), subscribers, messages, len(s.visitors))
|
s.messages, len(s.topics), subscribers, messages, len(s.visitors))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) sendDelayedMessages() error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
messages, err := s.cache.MessagesDue()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, m := range messages {
|
||||||
|
t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published
|
||||||
|
if ok {
|
||||||
|
if err := t.Publish(m); err != nil {
|
||||||
|
log.Printf("unable to publish message %s to topic %s: %v", m.ID, m.Topic, err.Error())
|
||||||
|
}
|
||||||
|
if s.firebase != nil {
|
||||||
|
if err := s.firebase(m); err != nil {
|
||||||
|
log.Printf("unable to publish to Firebase: %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.cache.MarkPublished(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) withRateLimit(w http.ResponseWriter, r *http.Request, handler func(w http.ResponseWriter, r *http.Request, v *visitor) error) error {
|
func (s *Server) withRateLimit(w http.ResponseWriter, r *http.Request, handler func(w http.ResponseWriter, r *http.Request, v *visitor) error) error {
|
||||||
v := s.visitor(r)
|
v := s.visitor(r)
|
||||||
if err := v.RequestAllowed(); err != nil {
|
if err := v.RequestAllowed(); err != nil {
|
||||||
|
|
Loading…
Reference in a new issue