better
This commit is contained in:
62
pkg/actor/disk_storage.go
Normal file
62
pkg/actor/disk_storage.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package actor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
type DiskStorage[V any] struct {
|
||||
*StateStorage
|
||||
path string
|
||||
}
|
||||
|
||||
type LogStorage[V any] interface {
|
||||
LoadEvents(id uint64, grain Grain[V]) error
|
||||
AppendEvent(id uint64, msg proto.Message) error
|
||||
}
|
||||
|
||||
func NewDiskStorage[V any](path string, registry MutationRegistry) LogStorage[V] {
|
||||
return &DiskStorage[V]{
|
||||
StateStorage: NewState(registry),
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DiskStorage[V]) logPath(id uint64) string {
|
||||
return filepath.Join(s.path, fmt.Sprintf("%d.events.log", id))
|
||||
}
|
||||
|
||||
func (s *DiskStorage[V]) LoadEvents(id uint64, grain Grain[V]) error {
|
||||
path := s.logPath(id)
|
||||
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
|
||||
// No log -> nothing to replay
|
||||
return nil
|
||||
}
|
||||
|
||||
fh, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open replay file: %w", err)
|
||||
}
|
||||
defer fh.Close()
|
||||
return s.Load(fh, func(msg proto.Message) {
|
||||
s.registry.Apply(grain, msg)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DiskStorage[V]) AppendEvent(id uint64, msg proto.Message) error {
|
||||
path := s.logPath(id)
|
||||
fh, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Printf("failed to open event log file: %v", err)
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
return s.Append(fh, msg)
|
||||
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package actor
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
//"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
type Grain[V any] interface {
|
||||
GetId() uint64
|
||||
Apply(content any, isReplay bool) (*V, error)
|
||||
//Apply(content proto.Message, isReplay bool) (*V, error)
|
||||
GetLastAccess() time.Time
|
||||
GetLastChange() time.Time
|
||||
GetCurrentState() (*V, error)
|
||||
|
||||
@@ -2,10 +2,12 @@ package actor
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
type GrainPool[V any] interface {
|
||||
Apply(id uint64, mutation any) (V, error)
|
||||
Apply(id uint64, mutation proto.Message) (V, error)
|
||||
Get(id uint64) (V, error)
|
||||
OwnerHost(id uint64) (Host, bool)
|
||||
Hostname() string
|
||||
|
||||
204
pkg/actor/mutation_registry.go
Normal file
204
pkg/actor/mutation_registry.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package actor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
type MutationRegistry interface {
|
||||
Apply(grain any, msg proto.Message) error
|
||||
RegisterMutations(handlers ...MutationHandler)
|
||||
Create(typeName string) (proto.Message, bool)
|
||||
GetTypeName(msg proto.Message) (string, bool)
|
||||
//GetStorageEvent(msg proto.Message) StorageEvent
|
||||
//FromStorageEvent(event StorageEvent) (proto.Message, error)
|
||||
}
|
||||
|
||||
type ProtoMutationRegistry struct {
|
||||
mutationRegistryMu sync.RWMutex
|
||||
mutationRegistry map[reflect.Type]MutationHandler
|
||||
}
|
||||
|
||||
var (
|
||||
ErrMutationNotRegistered = fmt.Errorf("mutation not registered")
|
||||
)
|
||||
|
||||
// MutationOption configures additional behavior for a registered mutation.
|
||||
type MutationOption func(*mutationOptions)
|
||||
|
||||
// mutationOptions holds flags adjustable per registration.
|
||||
type mutationOptions struct {
|
||||
updateTotals bool
|
||||
}
|
||||
|
||||
// WithTotals ensures CartGrain.UpdateTotals() is called after a successful handler.
|
||||
func WithTotals() MutationOption {
|
||||
return func(o *mutationOptions) {
|
||||
o.updateTotals = true
|
||||
}
|
||||
}
|
||||
|
||||
type MutationHandler interface {
|
||||
Handle(state any, msg proto.Message) error
|
||||
Name() string
|
||||
Type() reflect.Type
|
||||
Create() proto.Message
|
||||
}
|
||||
|
||||
// RegisteredMutation stores metadata + the execution closure.
|
||||
type RegisteredMutation[V any, T proto.Message] struct {
|
||||
name string
|
||||
handler func(*V, T) error
|
||||
create func() T
|
||||
msgType reflect.Type
|
||||
}
|
||||
|
||||
func NewMutation[V any, T proto.Message](handler func(*V, T) error, create func() T) *RegisteredMutation[V, T] {
|
||||
// Derive the name and message type from a concrete instance produced by create().
|
||||
// This avoids relying on reflect.TypeFor (which can yield unexpected results in some toolchains)
|
||||
// and ensures we always peel off the pointer layer for proto messages.
|
||||
instance := create()
|
||||
rt := reflect.TypeOf(instance)
|
||||
if rt.Kind() == reflect.Ptr {
|
||||
rt = rt.Elem()
|
||||
}
|
||||
return &RegisteredMutation[V, T]{
|
||||
name: rt.Name(),
|
||||
handler: handler,
|
||||
create: create,
|
||||
msgType: rt,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RegisteredMutation[V, T]) Handle(state any, msg proto.Message) error {
|
||||
return m.handler(state.(*V), msg.(T))
|
||||
}
|
||||
|
||||
func (m *RegisteredMutation[V, T]) Name() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
func (m *RegisteredMutation[V, T]) Create() proto.Message {
|
||||
return m.create()
|
||||
}
|
||||
|
||||
func (m *RegisteredMutation[V, T]) Type() reflect.Type {
|
||||
return m.msgType
|
||||
}
|
||||
|
||||
func NewMutationRegistry() MutationRegistry {
|
||||
return &ProtoMutationRegistry{
|
||||
mutationRegistry: make(map[reflect.Type]MutationHandler),
|
||||
mutationRegistryMu: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProtoMutationRegistry) RegisterMutations(handlers ...MutationHandler) {
|
||||
r.mutationRegistryMu.Lock()
|
||||
defer r.mutationRegistryMu.Unlock()
|
||||
|
||||
for _, handler := range handlers {
|
||||
r.mutationRegistry[handler.Type()] = handler
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProtoMutationRegistry) GetTypeName(msg proto.Message) (string, bool) {
|
||||
r.mutationRegistryMu.RLock()
|
||||
defer r.mutationRegistryMu.RUnlock()
|
||||
|
||||
rt := indirectType(reflect.TypeOf(msg))
|
||||
if handler, ok := r.mutationRegistry[rt]; ok {
|
||||
return handler.Name(), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (r *ProtoMutationRegistry) getHandler(typeName string) MutationHandler {
|
||||
r.mutationRegistryMu.Lock()
|
||||
defer r.mutationRegistryMu.Unlock()
|
||||
|
||||
for _, handler := range r.mutationRegistry {
|
||||
if handler.Name() == typeName {
|
||||
return handler
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ProtoMutationRegistry) Create(typeName string) (proto.Message, bool) {
|
||||
|
||||
handler := r.getHandler(typeName)
|
||||
if handler == nil {
|
||||
log.Printf("missing handler for %s", typeName)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return handler.Create(), true
|
||||
}
|
||||
|
||||
// ApplyRegistered attempts to apply a registered mutation.
|
||||
// Returns updated grain if successful.
|
||||
//
|
||||
// If the mutation is not registered, returns (nil, ErrMutationNotRegistered).
|
||||
func (r *ProtoMutationRegistry) Apply(grain any, msg proto.Message) error {
|
||||
if grain == nil {
|
||||
return fmt.Errorf("nil grain")
|
||||
}
|
||||
if msg == nil {
|
||||
return fmt.Errorf("nil mutation message")
|
||||
}
|
||||
|
||||
rt := indirectType(reflect.TypeOf(msg))
|
||||
r.mutationRegistryMu.RLock()
|
||||
entry, ok := r.mutationRegistry[rt]
|
||||
r.mutationRegistryMu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return ErrMutationNotRegistered
|
||||
}
|
||||
|
||||
if err := entry.Handle(grain, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if entry.updateTotals {
|
||||
// grain.UpdateTotals()
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisteredMutations returns metadata for all registered mutations (snapshot).
|
||||
func (r *ProtoMutationRegistry) RegisteredMutations() []string {
|
||||
r.mutationRegistryMu.RLock()
|
||||
defer r.mutationRegistryMu.RUnlock()
|
||||
out := make([]string, 0, len(r.mutationRegistry))
|
||||
for _, entry := range r.mutationRegistry {
|
||||
out = append(out, entry.Name())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// RegisteredMutationTypes returns the reflect.Type list of all registered messages.
|
||||
// Useful for coverage tests ensuring expected set matches actual set.
|
||||
func (r *ProtoMutationRegistry) RegisteredMutationTypes() []reflect.Type {
|
||||
r.mutationRegistryMu.RLock()
|
||||
defer r.mutationRegistryMu.RUnlock()
|
||||
out := make([]reflect.Type, 0, len(r.mutationRegistry))
|
||||
for _, entry := range r.mutationRegistry {
|
||||
out = append(out, entry.Type())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func indirectType(t reflect.Type) reflect.Type {
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
return t
|
||||
}
|
||||
151
pkg/actor/mutation_registry_test.go
Normal file
151
pkg/actor/mutation_registry_test.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package actor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
)
|
||||
|
||||
type cartState struct {
|
||||
calls int
|
||||
lastAdded *messages.AddItem
|
||||
}
|
||||
|
||||
func TestRegisteredMutationBasics(t *testing.T) {
|
||||
reg := NewMutationRegistry().(*ProtoMutationRegistry)
|
||||
|
||||
addItemMutation := NewMutation[cartState, *messages.AddItem](
|
||||
func(state *cartState, msg *messages.AddItem) error {
|
||||
state.calls++
|
||||
// copy to avoid external mutation side-effects (not strictly necessary for the test)
|
||||
cp := *msg
|
||||
state.lastAdded = &cp
|
||||
return nil
|
||||
},
|
||||
func() *messages.AddItem { return &messages.AddItem{} },
|
||||
)
|
||||
|
||||
// Sanity check on mutation metadata
|
||||
if addItemMutation.Name() != "AddItem" {
|
||||
t.Fatalf("expected mutation Name() == AddItem, got %s", addItemMutation.Name())
|
||||
}
|
||||
if got, want := addItemMutation.Type(), reflect.TypeOf(messages.AddItem{}); got != want {
|
||||
t.Fatalf("expected Type() == %v, got %v", want, got)
|
||||
}
|
||||
|
||||
reg.RegisterMutations(addItemMutation)
|
||||
|
||||
// RegisteredMutations: membership (order not guaranteed)
|
||||
names := reg.RegisteredMutations()
|
||||
if !stringSliceContains(names, "AddItem") {
|
||||
t.Fatalf("RegisteredMutations missing AddItem, got %v", names)
|
||||
}
|
||||
|
||||
// RegisteredMutationTypes: membership (order not guaranteed)
|
||||
types := reg.RegisteredMutationTypes()
|
||||
if !typeSliceContains(types, reflect.TypeOf(messages.AddItem{})) {
|
||||
t.Fatalf("RegisteredMutationTypes missing AddItem type, got %v", types)
|
||||
}
|
||||
|
||||
// GetTypeName should resolve for a pointer instance
|
||||
name, ok := reg.GetTypeName(&messages.AddItem{})
|
||||
if !ok || name != "AddItem" {
|
||||
t.Fatalf("GetTypeName returned (%q,%v), expected (AddItem,true)", name, ok)
|
||||
}
|
||||
|
||||
// GetTypeName should fail for unregistered type
|
||||
if name, ok := reg.GetTypeName(&messages.Noop{}); ok || name != "" {
|
||||
t.Fatalf("expected GetTypeName to fail for unregistered message, got (%q,%v)", name, ok)
|
||||
}
|
||||
|
||||
// Create by name
|
||||
msg, ok := reg.Create("AddItem")
|
||||
if !ok {
|
||||
t.Fatalf("Create failed for registered mutation")
|
||||
}
|
||||
if _, isAddItem := msg.(*messages.AddItem); !isAddItem {
|
||||
t.Fatalf("Create returned wrong concrete type: %T", msg)
|
||||
}
|
||||
|
||||
// Create unknown
|
||||
if m2, ok := reg.Create("Unknown"); ok || m2 != nil {
|
||||
t.Fatalf("Create should fail for unknown mutation, got (%T,%v)", m2, ok)
|
||||
}
|
||||
|
||||
// Apply happy path
|
||||
state := &cartState{}
|
||||
add := &messages.AddItem{ItemId: 42, Quantity: 3, Sku: "ABC"}
|
||||
if err := reg.Apply(state, add); err != nil {
|
||||
t.Fatalf("Apply returned error: %v", err)
|
||||
}
|
||||
if state.calls != 1 {
|
||||
t.Fatalf("handler not invoked expected calls=1 got=%d", state.calls)
|
||||
}
|
||||
if state.lastAdded == nil || state.lastAdded.ItemId != 42 || state.lastAdded.Quantity != 3 {
|
||||
t.Fatalf("state not updated correctly: %+v", state.lastAdded)
|
||||
}
|
||||
|
||||
// Apply nil grain
|
||||
if err := reg.Apply(nil, add); err == nil {
|
||||
t.Fatalf("expected error for nil grain")
|
||||
}
|
||||
|
||||
// Apply nil message
|
||||
if err := reg.Apply(state, nil); err == nil {
|
||||
t.Fatalf("expected error for nil mutation message")
|
||||
}
|
||||
|
||||
// Apply unregistered message
|
||||
if err := reg.Apply(state, &messages.Noop{}); !errors.Is(err, ErrMutationNotRegistered) {
|
||||
t.Fatalf("expected ErrMutationNotRegistered, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// func TestConcurrentSafeRegistrationLookup(t *testing.T) {
|
||||
// // This test is light-weight; it ensures locks don't deadlock under simple concurrent access.
|
||||
// reg := NewMutationRegistry().(*ProtoMutationRegistry)
|
||||
// mut := NewMutation[cartState, *messages.Noop](
|
||||
// func(state *cartState, msg *messages.Noop) error { state.calls++; return nil },
|
||||
// func() *messages.Noop { return &messages.Noop{} },
|
||||
// )
|
||||
// reg.RegisterMutations(mut)
|
||||
|
||||
// done := make(chan struct{})
|
||||
// const workers = 25
|
||||
// for i := 0; i < workers; i++ {
|
||||
// go func() {
|
||||
// for j := 0; j < 100; j++ {
|
||||
// _, _ = reg.Create("Noop")
|
||||
// _, _ = reg.GetTypeName(&messages.Noop{})
|
||||
// _ = reg.Apply(&cartState{}, &messages.Noop{})
|
||||
// }
|
||||
// done <- struct{}{}
|
||||
// }()
|
||||
// }
|
||||
|
||||
// for i := 0; i < workers; i++ {
|
||||
// <-done
|
||||
// }
|
||||
// }
|
||||
|
||||
// Helpers
|
||||
|
||||
func stringSliceContains(list []string, target string) bool {
|
||||
for _, s := range list {
|
||||
if s == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func typeSliceContains(list []reflect.Type, target reflect.Type) bool {
|
||||
for _, t := range list {
|
||||
if t == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -6,17 +6,20 @@ import (
|
||||
"maps"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
type SimpleGrainPool[V any] struct {
|
||||
// fields and methods
|
||||
localMu sync.RWMutex
|
||||
grains map[uint64]Grain[V]
|
||||
|
||||
spawn func(id uint64) (Grain[V], error)
|
||||
spawnHost func(host string) (Host, error)
|
||||
ttl time.Duration
|
||||
poolSize int
|
||||
localMu sync.RWMutex
|
||||
grains map[uint64]Grain[V]
|
||||
mutationRegistry MutationRegistry
|
||||
spawn func(id uint64) (Grain[V], error)
|
||||
spawnHost func(host string) (Host, error)
|
||||
storage LogStorage[V]
|
||||
ttl time.Duration
|
||||
poolSize int
|
||||
|
||||
// Cluster coordination --------------------------------------------------
|
||||
hostname string
|
||||
@@ -29,17 +32,28 @@ type SimpleGrainPool[V any] struct {
|
||||
purgeTicker *time.Ticker
|
||||
}
|
||||
|
||||
func NewSimpleGrainPool[V any](size int, ttl time.Duration, hostname string, spawn func(id uint64) (Grain[V], error), spawnHost func(host string) (Host, error)) (*SimpleGrainPool[V], error) {
|
||||
p := &SimpleGrainPool[V]{
|
||||
grains: make(map[uint64]Grain[V]),
|
||||
type GrainPoolConfig[V any] struct {
|
||||
Hostname string
|
||||
Spawn func(id uint64) (Grain[V], error)
|
||||
SpawnHost func(host string) (Host, error)
|
||||
TTL time.Duration
|
||||
PoolSize int
|
||||
MutationRegistry MutationRegistry
|
||||
Storage LogStorage[V]
|
||||
}
|
||||
|
||||
spawn: spawn,
|
||||
spawnHost: spawnHost,
|
||||
ttl: ttl,
|
||||
poolSize: size,
|
||||
hostname: hostname,
|
||||
remoteOwners: make(map[uint64]Host),
|
||||
remoteHosts: make(map[string]Host),
|
||||
func NewSimpleGrainPool[V any](config GrainPoolConfig[V]) (*SimpleGrainPool[V], error) {
|
||||
p := &SimpleGrainPool[V]{
|
||||
grains: make(map[uint64]Grain[V]),
|
||||
mutationRegistry: config.MutationRegistry,
|
||||
storage: config.Storage,
|
||||
spawn: config.Spawn,
|
||||
spawnHost: config.SpawnHost,
|
||||
ttl: config.TTL,
|
||||
poolSize: config.PoolSize,
|
||||
hostname: config.Hostname,
|
||||
remoteOwners: make(map[uint64]Host),
|
||||
remoteHosts: make(map[string]Host),
|
||||
}
|
||||
|
||||
p.purgeTicker = time.NewTicker(time.Minute)
|
||||
@@ -344,38 +358,22 @@ func (p *SimpleGrainPool[V]) getOrClaimGrain(id uint64) (Grain[V], error) {
|
||||
return grain, nil
|
||||
}
|
||||
|
||||
// ErrNotOwner is returned when a cart belongs to another host.
|
||||
var ErrNotOwner = fmt.Errorf("not owner")
|
||||
// // ErrNotOwner is returned when a cart belongs to another host.
|
||||
// var ErrNotOwner = fmt.Errorf("not owner")
|
||||
|
||||
// Apply applies a mutation to a grain.
|
||||
func (p *SimpleGrainPool[V]) Apply(id uint64, mutation any) (*V, error) {
|
||||
func (p *SimpleGrainPool[V]) Apply(id uint64, mutation proto.Message) (*V, error) {
|
||||
grain, err := p.getOrClaimGrain(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//start := time.Now()
|
||||
result, applyErr := grain.Apply(mutation, false)
|
||||
//mutationType := "unknown"
|
||||
// if mutation != nil {
|
||||
// if t := reflect.TypeOf(mutation); t != nil {
|
||||
// if t.Kind() == reflect.Pointer {
|
||||
// t = t.Elem()
|
||||
// }
|
||||
// if t.Name() != "" {
|
||||
// mutationType = t.Name()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// cartMutationLatencySeconds.WithLabelValues(mutationType).Observe(time.Since(start).Seconds())
|
||||
|
||||
// if applyErr == nil && result != nil {
|
||||
// cartMutationsTotal.Inc()
|
||||
|
||||
// } else if applyErr != nil {
|
||||
// cartMutationFailuresTotal.Inc()
|
||||
// }
|
||||
|
||||
return result, applyErr
|
||||
if applyErr := p.mutationRegistry.Apply(grain, mutation); applyErr != nil {
|
||||
return nil, applyErr
|
||||
}
|
||||
if err := p.storage.AppendEvent(id, mutation); err != nil {
|
||||
log.Printf("failed to store mutation for grain %d: %v", id, err)
|
||||
}
|
||||
return grain.GetCurrentState()
|
||||
}
|
||||
|
||||
// Get returns the current state of a grain.
|
||||
|
||||
86
pkg/actor/state.go
Normal file
86
pkg/actor/state.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package actor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
type StateStorage struct {
|
||||
registry MutationRegistry
|
||||
}
|
||||
|
||||
type StorageEvent struct {
|
||||
Type string `json:"type"`
|
||||
TimeStamp time.Time `json:"timestamp"`
|
||||
Mutation proto.Message `json:"mutation"`
|
||||
}
|
||||
|
||||
type rawEvent struct {
|
||||
Type string `json:"type"`
|
||||
TimeStamp time.Time `json:"timestamp"`
|
||||
Mutation json.RawMessage `json:"mutation"`
|
||||
}
|
||||
|
||||
func NewState(registry MutationRegistry) *StateStorage {
|
||||
return &StateStorage{
|
||||
registry: registry,
|
||||
}
|
||||
}
|
||||
|
||||
var ErrUnknownType = errors.New("unknown type")
|
||||
|
||||
func (s *StateStorage) Load(r io.Reader, onMessage func(msg proto.Message)) error {
|
||||
var err error
|
||||
for err == nil {
|
||||
evt, err := s.Read(r)
|
||||
if err == nil {
|
||||
onMessage(evt.Mutation)
|
||||
}
|
||||
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *StateStorage) Append(io io.Writer, mutation proto.Message) error {
|
||||
typeName, ok := s.registry.GetTypeName(mutation)
|
||||
if !ok {
|
||||
return ErrUnknownType
|
||||
}
|
||||
event := &StorageEvent{
|
||||
Type: typeName,
|
||||
TimeStamp: time.Now(),
|
||||
Mutation: mutation,
|
||||
}
|
||||
jsonBytes, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Write(jsonBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StateStorage) Read(r io.Reader) (*StorageEvent, error) {
|
||||
var event rawEvent
|
||||
if err := json.NewDecoder(r).Decode(&event); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
typeName := event.Type
|
||||
mutation, ok := s.registry.Create(typeName)
|
||||
if !ok {
|
||||
return nil, ErrUnknownType
|
||||
}
|
||||
if err := json.Unmarshal(event.Mutation, mutation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &StorageEvent{
|
||||
Type: typeName,
|
||||
TimeStamp: event.TimeStamp,
|
||||
Mutation: mutation,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user