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
-
8internal/auth/token.go
-
44internal/notifier/notifier.go
-
128models/change.go
-
18models/changes/db.go
-
13repositories/change.go
-
1repositories/repository.go
-
133services/changes.go
-
21services/characters.go
-
5services/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