Browse Source

Changes to change system.

thegreatrefactor
Gisle Aune 6 years ago
parent
commit
021f67ea4b
  1. 5
      .idea/codeStyles/codeStyleConfig.xml
  2. 6
      .idea/inspectionProfiles/Project_Default.xml
  3. 7
      cmd/rpdata-server/main.go
  4. 104
      database/mongodb/changes.go
  5. 23
      database/mongodb/db.go
  6. 3
      graph2/complexity.go
  7. 2
      graph2/gqlgen.yml
  8. 30
      graph2/resolvers/changes.go
  9. 2
      graph2/schema/root.gql
  10. 5
      graph2/schema/types/Change.gql
  11. 8
      internal/auth/token.go
  12. 44
      internal/notifier/notifier.go
  13. 128
      models/change.go
  14. 18
      models/changes/db.go
  15. 13
      repositories/change.go
  16. 1
      repositories/repository.go
  17. 133
      services/changes.go
  18. 21
      services/characters.go
  19. 5
      services/services.go

5
.idea/codeStyles/codeStyleConfig.xml

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

6
.idea/inspectionProfiles/Project_Default.xml

@ -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>

7
cmd/rpdata-server/main.go

@ -16,7 +16,6 @@ import (
"git.aiterp.net/rpdata/api/internal/loader"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/changes"
"git.aiterp.net/rpdata/api/models/logs"
"git.aiterp.net/rpdata/api/services"
"github.com/99designs/gqlgen/handler"
@ -52,13 +51,13 @@ func main() {
log.Println("Characters updated")
}()
go logListedChanges()
go logListedChanges(services.Changes)
log.Fatal(http.ListenAndServe(":8081", nil))
}
func logListedChanges() {
sub := changes.Subscribe(context.Background(), nil, true)
func logListedChanges(changes *services.ChangeService) {
sub := changes.Subscribe(context.Background(), models.ChangeFilter{PassAll: true})
for change := range sub {
log.Printf("Change: Author=%#+v Model=%#+v Op=%#+v", change.Author, change.Model, change.Op)

104
database/mongodb/changes.go

@ -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
}

23
database/mongodb/db.go

@ -38,8 +38,15 @@ func Init(cfg config.Database) (bundle *repositories.Bundle, closeFn func() erro
return nil, nil, err
}
changes, err := newChangeRepository(db)
if err != nil {
session.Close()
return nil, nil, err
}
bundle = &repositories.Bundle{
Characters: characters,
Changes: changes,
Tags: newTagRepository(db),
}
@ -73,6 +80,22 @@ func (c *counter) WithName(name string) *counter {
}
}
func (c *counter) WithCategory(category string) *counter {
return &counter{
coll: c.coll,
category: category,
name: c.name,
}
}
func (c *counter) With(category, name string) *counter {
return &counter{
coll: c.coll,
category: category,
name: name,
}
}
func (c *counter) Increment(amount int) (int, error) {
type counterDoc struct {
ID string `bson:"_id"`

3
graph2/complexity.go

@ -3,7 +3,6 @@ package graph2
import (
"git.aiterp.net/rpdata/api/graph2/graphcore"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/changes"
"git.aiterp.net/rpdata/api/models/channels"
"git.aiterp.net/rpdata/api/models/files"
"git.aiterp.net/rpdata/api/models/logs"
@ -71,7 +70,7 @@ func complexity() (cr graphcore.ComplexityRoot) {
cr.Query.Files = func(childComplexity int, filter *files.Filter) int {
return childComplexity + listComplexity
}
cr.Query.Changes = func(childComplexity int, filter *changes.Filter) int {
cr.Query.Changes = func(childComplexity int, filter *models.ChangeFilter) int {
return childComplexity + listComplexity
}
cr.Query.Token = func(childComplexity int) int {

2
graph2/gqlgen.yml

@ -62,7 +62,7 @@ models:
ChangeKeyInput: # It's the same as ChangeKey
model: git.aiterp.net/rpdata/api/models.ChangeKey
ChangesFilter:
model: git.aiterp.net/rpdata/api/models/changes.Filter
model: git.aiterp.net/rpdata/api/models.ChangeFilter
Token:
model: git.aiterp.net/rpdata/api/models.Token
User:

30
graph2/resolvers/changes.go

@ -2,39 +2,25 @@ package resolvers
import (
"context"
"errors"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/changes"
)
/// Queries
func (r *queryResolver) Changes(ctx context.Context, filter *changes.Filter) ([]*models.Change, error) {
changes, err := changes.List(filter)
if err != nil {
return nil, err
}
changes2 := make([]*models.Change, len(changes))
for i := range changes {
changes2[i] = &changes[i]
func (r *queryResolver) Changes(ctx context.Context, filter *models.ChangeFilter) ([]*models.Change, error) {
if filter == nil {
filter = &models.ChangeFilter{}
}
return changes2, nil
return r.s.Changes.List(ctx, *filter)
}
/// Subscriptions
func (r *subscriptionResolver) Changes(ctx context.Context, keys []*models.ChangeKey) (<-chan *models.Change, error) {
if len(keys) == 0 {
return nil, errors.New("At least one key is required for a subscription")
}
keys2 := make([]models.ChangeKey, len(keys))
for i := range keys {
keys2[i] = *keys[i]
func (r *subscriptionResolver) Changes(ctx context.Context, filter *models.ChangeFilter) (<-chan *models.Change, error) {
if filter == nil {
filter = &models.ChangeFilter{}
}
return changes.Subscribe(ctx, keys2, false), nil
return r.s.Changes.Subscribe(ctx, *filter), nil
}

2
graph2/schema/root.gql

@ -164,7 +164,7 @@ type Subscription {
"""
Changes subscribes to the changes matching the following keys.
"""
changes(keys: [ChangeKeyInput!]!): Change!
changes(filter: ChangesFilter): Change!
}
# A Time represents a RFC3339 encoded date with up to millisecond precision.

5
graph2/schema/types/Change.gql

@ -73,9 +73,12 @@ input ChangesFilter {
"The keys to query for."
keys: [ChangeKeyInput!]
"Only show changes by this author"
author: String
"Only show changes more recent than this date."
earliestDate: Time
"Limit the amount of results."
"Limit the amount of results. This even goes for subscriptions!"
limit: Int
}

8
internal/auth/token.go

@ -114,7 +114,8 @@ func CheckToken(tokenString string) (token models.Token, err error) {
return models.Token{}, ErrDeletedUser
}
acceptedPermissions := make([]string, 0, 8)
acceptedPermissions := make([]string, 0, len(user.Permissions))
if len(permissions) > 0 {
for _, permission := range permissions {
found := false
@ -129,6 +130,9 @@ func CheckToken(tokenString string) (token models.Token, err error) {
acceptedPermissions = append(acceptedPermissions, permission)
}
}
} else {
acceptedPermissions = append(acceptedPermissions, user.Permissions...)
}
return models.Token{UserID: user.ID, Permissions: acceptedPermissions}, nil
}
@ -153,11 +157,11 @@ func parseClaims(jwtClaims jwt.Claims) (userid string, permissions []string, err
permissions = append(permissions, permission)
}
}
}
if len(permissions) == 0 {
return "", nil, ErrInvalidClaims
}
}
return
}

44
internal/notifier/notifier.go

@ -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()
}
}

128
models/change.go

@ -1,6 +1,9 @@
package models
import "time"
import (
"reflect"
"time"
)
// Change represents a change in the rpdata history through the API.
type Change struct {
@ -22,15 +25,55 @@ type Change struct {
Comments []Comment `bson:"comments"`
}
// ChangeKey is a key for a change that can be used when subscribing to them.
type ChangeKey struct {
Model ChangeModel `bson:"model"`
ID string `bson:"id"`
// AddObject adds the model into the appropriate array.
func (change *Change) AddObject(object interface{}) bool {
if v := reflect.ValueOf(object); v.Kind() == reflect.Ptr {
return change.AddObject(v.Elem().Interface())
}
switch object := object.(type) {
case Log:
change.Logs = append(change.Logs, object)
case []Log:
change.Logs = append(change.Logs, object...)
case Character:
change.Characters = append(change.Characters, object)
case []Character:
change.Characters = append(change.Characters, object...)
case Channel:
change.Channels = append(change.Channels, object)
case []Channel:
change.Channels = append(change.Channels, object...)
case Post:
change.Posts = append(change.Posts, object)
case []Post:
change.Posts = append(change.Posts, object...)
case Story:
change.Stories = append(change.Stories, object)
case []Story:
change.Stories = append(change.Stories, object...)
case Tag:
change.Tags = append(change.Tags, object)
case []Tag:
change.Tags = append(change.Tags, object...)
case Chapter:
change.Chapters = append(change.Chapters, object)
case []Chapter:
change.Chapters = append(change.Chapters, object...)
case Comment:
change.Comments = append(change.Comments, object)
case []Comment:
change.Comments = append(change.Comments, object...)
default:
return false
}
return true
}
// Objects makes a combined, mixed array of all the models stored in this change.
func (change *Change) Objects() []interface{} {
data := make([]interface{}, 0, len(change.Logs)+len(change.Characters)+len(change.Channels)+len(change.Posts)+len(change.Stories)+len(change.Tags)+len(change.Chapters)+len(change.Comments))
data := make([]interface{}, 0, 8)
for _, log := range change.Logs {
data = append(data, &log)
@ -59,3 +102,76 @@ func (change *Change) Objects() []interface{} {
return data
}
func (change *Change) PassesFilter(filter ChangeFilter) bool {
if filter.PassAll {
return true
}
if filter.Author != nil && change.Author != *filter.Author {
return false
}
// For unlisted changes, pass it only if the filter refers to the specific index.
if !change.Listed {
hasSpecificKey := false
KeyFindLoop:
for _, key := range filter.Keys {
if key.ID == "*" {
continue
}
for _, changeKey := range change.Keys {
if changeKey.Model == key.Model && changeKey.ID == key.ID {
hasSpecificKey = true
break KeyFindLoop
}
}
}
if !hasSpecificKey {
return false
}
}
if filter.EarliestDate != nil && filter.EarliestDate.Before(change.Date) {
return false
}
if len(filter.Keys) > 0 {
foundKey := false
KeyFindLoop2:
for _, key := range filter.Keys {
for _, changeKey := range change.Keys {
if changeKey == key {
foundKey = true
break KeyFindLoop2
}
}
}
if !foundKey {
return false
}
}
return true
}
// ChangeKey is a key for a change that can be used when subscribing to them.
type ChangeKey struct {
Model ChangeModel `bson:"model"`
ID string `bson:"id"`
}
// ChangeFilter is a filter for listing changes.
type ChangeFilter struct {
Keys []ChangeKey
EarliestDate *time.Time
Author *string
Limit *int
PassAll bool // DO NOT EXPOSE
}

18
models/changes/db.go

@ -1,14 +1,11 @@
package changes
import (
"log"
"sync"
"time"
"git.aiterp.net/rpdata/api/internal/store"
"git.aiterp.net/rpdata/api/models"
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"sync"
)
var collection *mgo.Collection
@ -24,18 +21,5 @@ func list(query bson.M, limit int) ([]models.Change, error) {
func init() {
store.HandleInit(func(db *mgo.Database) {
collection = db.C("common.changes")
collection.EnsureIndexKey("date")
collection.EnsureIndexKey("author")
collection.EnsureIndexKey("keys")
err := collection.EnsureIndex(mgo.Index{
Name: "expiry",
Key: []string{"date"},
ExpireAfter: time.Hour * 2400, // 100 days
})
if err != nil {
log.Fatalln(err)
}
})
}

13
repositories/change.go

@ -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
}

1
repositories/repository.go

@ -5,6 +5,7 @@ import "errors"
// A Bundle is a set of repositories.
type Bundle struct {
Characters CharacterRepository
Changes ChangeRepository
Tags TagRepository
}

133
services/changes.go

@ -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()
}
}

21
services/characters.go

@ -6,7 +6,6 @@ import (
"git.aiterp.net/rpdata/api/internal/auth"
"git.aiterp.net/rpdata/api/models"
"git.aiterp.net/rpdata/api/models/changekeys"
"git.aiterp.net/rpdata/api/models/changes"
"git.aiterp.net/rpdata/api/repositories"
"git.aiterp.net/rpdata/api/services/loaders"
"sort"
@ -16,6 +15,7 @@ import (
type CharacterService struct {
characters repositories.CharacterRepository
loader *loaders.CharacterLoader
changeService *ChangeService
}
// Find uses the loader to find the character by the ID.
@ -101,8 +101,7 @@ func (s *CharacterService) Create(ctx context.Context, nick, name, shortName, au
return nil, err
}
//TODO: New change submit system
go changes.Submit("Character", "add", token.UserID, true, changekeys.Listed(character), character)
s.changeService.Submit(ctx, "Character", "add", true, changekeys.Listed(character), character)
return character, nil
}
@ -130,9 +129,7 @@ func (s *CharacterService) Update(ctx context.Context, id string, name, shortNam
s.loader.Clear(character.ID)
s.loader.Prime(character.ID, character)
//TODO: New change submit system
token := auth.TokenFromContext(ctx)
go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character)
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
return character, nil
}
@ -153,9 +150,7 @@ func (s *CharacterService) AddNick(ctx context.Context, id string, nick string)
return nil, err
}
//TODO: New change submit system
token := auth.TokenFromContext(ctx)
go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character)
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
return character, nil
}
@ -176,9 +171,7 @@ func (s *CharacterService) RemoveNick(ctx context.Context, id string, nick strin
return nil, err
}
//TODO: New change submit system
token := auth.TokenFromContext(ctx)
go changes.Submit("Character", "edit", token.UserID, true, changekeys.Listed(character), character)
s.changeService.Submit(ctx, "Character", "edit", true, changekeys.Listed(character), character)
return character, nil
}
@ -199,9 +192,7 @@ func (s *CharacterService) Delete(ctx context.Context, id string) (*models.Chara
return nil, err
}
//TODO: New change submit system
token := auth.TokenFromContext(ctx)
go changes.Submit("Character", "remove", token.UserID, true, changekeys.Listed(character), character)
s.changeService.Submit(ctx, "Character", "remove", true, changekeys.Listed(character), character)
return character, nil
}

5
services/services.go

@ -9,16 +9,21 @@ import (
type Bundle struct {
Tags *TagService
Characters *CharacterService
Changes *ChangeService
}
// NewBundle creates a new bundle.
func NewBundle(repos *repositories.Bundle) *Bundle {
bundle := &Bundle{}
bundle.Changes = &ChangeService{
changes: repos.Changes,
}
bundle.Tags = &TagService{tags: repos.Tags}
bundle.Characters = &CharacterService{
characters: repos.Characters,
loader: loaders.CharacterLoaderFromRepository(repos.Characters),
changeService: bundle.Changes,
}
return bundle

Loading…
Cancel
Save