Added change.Timeline, added change submissions to channel mutations, made all change submissions spin off a goroutine
the build was successful
the build was successful
This commit is contained in:
@@ -2,6 +2,7 @@ package change
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.aiterp.net/rpdata/api/model/counter"
|
||||
@@ -30,8 +31,13 @@ var PublicModels = []string{
|
||||
"Post",
|
||||
}
|
||||
|
||||
var submitMutex sync.Mutex
|
||||
|
||||
// Submit submits a change to the history.
|
||||
func Submit(model, op, author, objectID string, data interface{}) (Change, error) {
|
||||
submitMutex.Lock()
|
||||
defer submitMutex.Unlock()
|
||||
|
||||
index, err := counter.Next("auto_increment", "Change")
|
||||
if err != nil {
|
||||
return Change{}, err
|
||||
@@ -47,6 +53,8 @@ func Submit(model, op, author, objectID string, data interface{}) (Change, error
|
||||
Data: data,
|
||||
}
|
||||
|
||||
Timeline.push(change)
|
||||
|
||||
err = collection.Insert(&change)
|
||||
if err != nil {
|
||||
return Change{}, err
|
||||
@@ -55,6 +63,28 @@ func Submit(model, op, author, objectID string, data interface{}) (Change, error
|
||||
return change, err
|
||||
}
|
||||
|
||||
// FindID gets a change by ID
|
||||
func FindID(id int) (Change, error) {
|
||||
change := Change{}
|
||||
err := collection.FindId(id).One(&change)
|
||||
if err != nil {
|
||||
return Change{}, err
|
||||
}
|
||||
|
||||
return change, nil
|
||||
}
|
||||
|
||||
// List gets all changes in ascending order
|
||||
func List() ([]Change, error) {
|
||||
changes := make([]Change, 0, 64)
|
||||
err := collection.Find(nil).Sort("_id").All(&changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
store.HandleInit(func(db *mgo.Database) {
|
||||
collection = db.C("common.history")
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package change
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/globalsign/mgo"
|
||||
)
|
||||
|
||||
// ErrInvalidIndex is returned when a timeline subscriber tries to get a value
|
||||
// with a negative id
|
||||
var ErrInvalidIndex = errors.New("change.timeline: Invalid ID")
|
||||
|
||||
// ErrWaitRequired is returned when a timeline subscriber requests an entry that
|
||||
// has not yet been pushed
|
||||
var ErrWaitRequired = errors.New("change.timeline: Waiting required")
|
||||
|
||||
// ErrExpired is returned when a timeline subscriber requests a change that happened
|
||||
// too long ago for it to be in the database.
|
||||
var ErrExpired = errors.New("change.timeline: Change has expired")
|
||||
|
||||
type timeline struct {
|
||||
mutex sync.Mutex
|
||||
firstIndex int64
|
||||
currentID int64
|
||||
cache []Change
|
||||
notifyCh chan struct{}
|
||||
}
|
||||
|
||||
func (timeline *timeline) Get(ctx context.Context, id int64) (Change, error) {
|
||||
change, err := timeline.get(id, false)
|
||||
if err == ErrWaitRequired {
|
||||
if err := timeline.wait(ctx); err != nil {
|
||||
return Change{}, err
|
||||
}
|
||||
|
||||
return timeline.get(id, true)
|
||||
} else if err == mgo.ErrNotFound {
|
||||
return Change{}, ErrExpired
|
||||
}
|
||||
|
||||
return change, nil
|
||||
}
|
||||
|
||||
func (timeline *timeline) push(change Change) {
|
||||
timeline.mutex.Lock()
|
||||
timeline.cache = append(timeline.cache, change)
|
||||
if len(timeline.cache) > 128 {
|
||||
copy(timeline.cache, timeline.cache[32:])
|
||||
timeline.cache = timeline.cache[:len(timeline.cache)-32]
|
||||
}
|
||||
|
||||
// Only update the currentID if this is more recent than the existing ones
|
||||
if int64(change.ID) > timeline.currentID {
|
||||
timeline.currentID = int64(change.ID)
|
||||
}
|
||||
|
||||
if timeline.notifyCh != nil {
|
||||
close(timeline.notifyCh)
|
||||
timeline.notifyCh = nil
|
||||
}
|
||||
timeline.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (timeline *timeline) get(id int64, hasWaited bool) (Change, error) {
|
||||
if id < 0 {
|
||||
return Change{}, ErrInvalidIndex
|
||||
}
|
||||
|
||||
timeline.mutex.Lock()
|
||||
if !hasWaited && id > timeline.currentID {
|
||||
timeline.mutex.Unlock()
|
||||
return Change{}, ErrWaitRequired
|
||||
}
|
||||
|
||||
for _, change := range timeline.cache {
|
||||
if change.ID == int(id) {
|
||||
timeline.mutex.Unlock()
|
||||
return change, nil
|
||||
}
|
||||
}
|
||||
timeline.mutex.Unlock()
|
||||
|
||||
return FindID(int(id))
|
||||
}
|
||||
|
||||
func (timeline *timeline) wait(ctx context.Context) error {
|
||||
timeline.mutex.Lock()
|
||||
if timeline.notifyCh == nil {
|
||||
timeline.notifyCh = make(chan struct{})
|
||||
}
|
||||
|
||||
ch := timeline.notifyCh
|
||||
timeline.mutex.Unlock()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ch:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Timeline has operations for subscribing to changes in real time
|
||||
var Timeline = &timeline{}
|
||||
|
||||
// InitializeTimeline fills in the timeline with existing entries
|
||||
func InitializeTimeline() {
|
||||
changes, err := List()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, change := range changes {
|
||||
Timeline.push(change)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user