Gisle Aune
6 years ago
19 changed files with 503 additions and 87 deletions
-
5.idea/codeStyles/codeStyleConfig.xml
-
6.idea/inspectionProfiles/Project_Default.xml
-
7cmd/rpdata-server/main.go
-
104database/mongodb/changes.go
-
23database/mongodb/db.go
-
3graph2/complexity.go
-
2graph2/gqlgen.yml
-
30graph2/resolvers/changes.go
-
2graph2/schema/root.gql
-
5graph2/schema/types/Change.gql
-
32internal/auth/token.go
-
44internal/notifier/notifier.go
-
128models/change.go
-
18models/changes/db.go
-
13repositories/change.go
-
1repositories/repository.go
-
133services/changes.go
-
25services/characters.go
-
9services/services.go
@ -0,0 +1,5 @@ |
|||||
|
<component name="ProjectCodeStyleConfiguration"> |
||||
|
<state> |
||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> |
||||
|
</state> |
||||
|
</component> |
@ -0,0 +1,6 @@ |
|||||
|
<component name="InspectionProjectProfileManager"> |
||||
|
<profile version="1.0"> |
||||
|
<option name="myName" value="Project Default" /> |
||||
|
<inspection_tool class="GoNilness" enabled="false" level="WARNING" enabled_by_default="false" /> |
||||
|
</profile> |
||||
|
</component> |
@ -0,0 +1,104 @@ |
|||||
|
package mongodb |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
"git.aiterp.net/rpdata/api/repositories" |
||||
|
"github.com/globalsign/mgo" |
||||
|
"github.com/globalsign/mgo/bson" |
||||
|
"strconv" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
type changeRepository struct { |
||||
|
changes *mgo.Collection |
||||
|
idCounter *counter |
||||
|
} |
||||
|
|
||||
|
func (r *changeRepository) Find(ctx context.Context, id string) (*models.Change, error) { |
||||
|
change := new(models.Change) |
||||
|
err := r.changes.FindId(id).One(change) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return change, nil |
||||
|
} |
||||
|
|
||||
|
func (r *changeRepository) List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) { |
||||
|
query := bson.M{} |
||||
|
limit := 0 |
||||
|
if filter.EarliestDate != nil && !filter.EarliestDate.IsZero() { |
||||
|
query["date"] = bson.M{"$gte": *filter.EarliestDate} |
||||
|
} |
||||
|
if len(filter.Keys) > 0 { |
||||
|
query["keys"] = bson.M{"$in": filter.Keys} |
||||
|
} |
||||
|
if filter.Author != nil && *filter.Author != "" { |
||||
|
query["author"] = *filter.Author |
||||
|
} |
||||
|
if filter.Limit != nil { |
||||
|
limit = *filter.Limit |
||||
|
} |
||||
|
|
||||
|
initialSize := 64 |
||||
|
if limit > 0 && limit < 256 { |
||||
|
initialSize = limit |
||||
|
} |
||||
|
|
||||
|
changes := make([]*models.Change, 0, initialSize) |
||||
|
err := r.changes.Find(query).All(&changes) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return changes, nil |
||||
|
} |
||||
|
|
||||
|
func (r *changeRepository) Insert(ctx context.Context, change models.Change) (*models.Change, error) { |
||||
|
next, err := r.idCounter.Increment(1) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
change.ID = "Change_" + strconv.Itoa(next) |
||||
|
|
||||
|
err = r.changes.Insert(change) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return &change, nil |
||||
|
} |
||||
|
|
||||
|
func (r *changeRepository) Remove(ctx context.Context, change models.Change) error { |
||||
|
return r.changes.RemoveId(change.ID) |
||||
|
} |
||||
|
|
||||
|
func newChangeRepository(db *mgo.Database) (repositories.ChangeRepository, error) { |
||||
|
collection := db.C("common.changes") |
||||
|
|
||||
|
err := collection.EnsureIndex(mgo.Index{ |
||||
|
Name: "expiry", |
||||
|
Key: []string{"date"}, |
||||
|
ExpireAfter: time.Hour * 2400, // 100 days
|
||||
|
}) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
err = collection.EnsureIndexKey("author") |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
err = collection.EnsureIndexKey("keys") |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
return &changeRepository{ |
||||
|
changes: collection, |
||||
|
idCounter: newCounter(db, "auto_increment", "Change"), |
||||
|
}, nil |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
package notifier |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"sync" |
||||
|
) |
||||
|
|
||||
|
// A Notifier is a synchronization primitive for waking upp all listeners at once.
|
||||
|
type Notifier struct { |
||||
|
mutex sync.Mutex |
||||
|
ch chan struct{} |
||||
|
} |
||||
|
|
||||
|
// Broadcast wakes all listeners if there are any.
|
||||
|
func (notifier *Notifier) Broadcast() { |
||||
|
notifier.mutex.Lock() |
||||
|
if notifier.ch != nil { |
||||
|
close(notifier.ch) |
||||
|
notifier.ch = nil |
||||
|
} |
||||
|
notifier.mutex.Unlock() |
||||
|
} |
||||
|
|
||||
|
// C gets the channel that'll close on the next notification.
|
||||
|
func (notifier *Notifier) C() <-chan struct{} { |
||||
|
notifier.mutex.Lock() |
||||
|
if notifier.ch == nil { |
||||
|
notifier.ch = make(chan struct{}) |
||||
|
} |
||||
|
ch := notifier.ch |
||||
|
notifier.mutex.Unlock() |
||||
|
|
||||
|
return ch |
||||
|
} |
||||
|
|
||||
|
// Wait waits for the next `Broadcast` call, or the context's termination.
|
||||
|
func (notifier *Notifier) Wait(ctx context.Context) error { |
||||
|
select { |
||||
|
case <-notifier.C(): |
||||
|
return nil |
||||
|
case <-ctx.Done(): |
||||
|
return ctx.Err() |
||||
|
} |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
package repositories |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
) |
||||
|
|
||||
|
type ChangeRepository interface { |
||||
|
Find(ctx context.Context, id string) (*models.Change, error) |
||||
|
List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) |
||||
|
Insert(ctx context.Context, change models.Change) (*models.Change, error) |
||||
|
Remove(ctx context.Context, change models.Change) error |
||||
|
} |
@ -0,0 +1,133 @@ |
|||||
|
package services |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"git.aiterp.net/rpdata/api/internal/auth" |
||||
|
"git.aiterp.net/rpdata/api/internal/notifier" |
||||
|
"git.aiterp.net/rpdata/api/models" |
||||
|
"git.aiterp.net/rpdata/api/repositories" |
||||
|
"log" |
||||
|
"sync" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
type ChangeService struct { |
||||
|
changes repositories.ChangeRepository |
||||
|
|
||||
|
mutex sync.RWMutex |
||||
|
buffer []models.Change |
||||
|
offset uint64 |
||||
|
notifier notifier.Notifier |
||||
|
submitQueue chan *models.Change |
||||
|
loopStarted bool |
||||
|
} |
||||
|
|
||||
|
func (s *ChangeService) Find(ctx context.Context, id string) (*models.Change, error) { |
||||
|
return s.changes.Find(ctx, id) |
||||
|
} |
||||
|
|
||||
|
func (s *ChangeService) List(ctx context.Context, filter models.ChangeFilter) ([]*models.Change, error) { |
||||
|
return s.changes.List(ctx, filter) |
||||
|
} |
||||
|
|
||||
|
func (s *ChangeService) Submit(ctx context.Context, model models.ChangeModel, op string, listed bool, keys []models.ChangeKey, objects ...interface{}) { |
||||
|
token := auth.TokenFromContext(ctx) |
||||
|
if token == nil { |
||||
|
panic("no token!") |
||||
|
} |
||||
|
|
||||
|
change := &models.Change{ |
||||
|
Model: model, |
||||
|
Op: op, |
||||
|
Author: token.UserID, |
||||
|
Listed: listed, |
||||
|
Keys: keys, |
||||
|
} |
||||
|
|
||||
|
for _, obj := range objects { |
||||
|
if !change.AddObject(obj) { |
||||
|
log.Printf("Cannot add object of type %T to change", obj) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
s.mutex.Lock() |
||||
|
if !s.loopStarted { |
||||
|
s.loopStarted = true |
||||
|
s.submitQueue = make(chan *models.Change, 64) |
||||
|
go s.loop() |
||||
|
} |
||||
|
s.mutex.Unlock() |
||||
|
|
||||
|
s.submitQueue <- change |
||||
|
} |
||||
|
|
||||
|
func (s *ChangeService) Subscribe(ctx context.Context, filter models.ChangeFilter) <-chan *models.Change { |
||||
|
channel := make(chan *models.Change) |
||||
|
|
||||
|
go func() { |
||||
|
defer close(channel) |
||||
|
|
||||
|
s.mutex.RLock() |
||||
|
pos := s.offset + uint64(len(s.buffer)) |
||||
|
slice := make([]models.Change, 32) |
||||
|
s.mutex.RUnlock() |
||||
|
|
||||
|
count := 0 |
||||
|
|
||||
|
for { |
||||
|
s.mutex.RLock() |
||||
|
nextPos := s.offset + uint64(len(s.buffer)) |
||||
|
length := nextPos - pos |
||||
|
if length > 0 { |
||||
|
index := pos - s.offset |
||||
|
|
||||
|
pos = nextPos |
||||
|
copy(slice, s.buffer[index:]) |
||||
|
} |
||||
|
ch := s.notifier.C() |
||||
|
s.mutex.RUnlock() |
||||
|
|
||||
|
for _, change := range slice[:length] { |
||||
|
if change.PassesFilter(filter) { |
||||
|
channel <- &change |
||||
|
|
||||
|
count++ |
||||
|
if filter.Limit != nil && count == *filter.Limit { |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
select { |
||||
|
case <-ch: |
||||
|
case <-ctx.Done(): |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
return channel |
||||
|
} |
||||
|
|
||||
|
func (s *ChangeService) loop() { |
||||
|
for change := range s.submitQueue { |
||||
|
timeout, cancel := context.WithTimeout(context.Background(), time.Second*15) |
||||
|
|
||||
|
change, err := s.changes.Insert(timeout, *change) |
||||
|
if err != nil { |
||||
|
log.Println("Failed to submit change:") |
||||
|
} |
||||
|
|
||||
|
s.mutex.Lock() |
||||
|
s.buffer = append(s.buffer, *change) |
||||
|
if len(s.buffer) > 16 { |
||||
|
copy(s.buffer, s.buffer[8:]) |
||||
|
s.buffer = s.buffer[:len(s.buffer)-8] |
||||
|
s.offset += 8 |
||||
|
} |
||||
|
s.mutex.Unlock() |
||||
|
s.notifier.Broadcast() |
||||
|
|
||||
|
cancel() |
||||
|
} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue