Refactoring: re-organize project intro multiple packages (folders)
This commit is contained in:
191
storage/dailydisk.go
Normal file
191
storage/dailydisk.go
Normal file
@ -0,0 +1,191 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"path"
|
||||
"os"
|
||||
"time"
|
||||
"github.com/satori/go.uuid"
|
||||
"fmt"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type DailyDiskStorage struct {
|
||||
storagePath string
|
||||
indexesPath string
|
||||
globalIndexFilename string
|
||||
}
|
||||
|
||||
func NewDailyDiskStorage(storagePath string) Storage {
|
||||
indexesPath := path.Join(storagePath, "indexes")
|
||||
globalIndexPath := path.Join(indexesPath, "global")
|
||||
if err := os.MkdirAll(indexesPath, 0777); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &DailyDiskStorage{storagePath, indexesPath, globalIndexPath};
|
||||
}
|
||||
|
||||
func (me DailyDiskStorage) getStreamIndexFilename(streamId uuid.UUID) string {
|
||||
return path.Join(me.indexesPath, streamId.String())
|
||||
}
|
||||
|
||||
func (me DailyDiskStorage) getEventFilename(creationTime time.Time, typeId string) string {
|
||||
yearMonth := fmt.Sprintf("%04d%02d", creationTime.Year(), creationTime.Month())
|
||||
day := fmt.Sprintf("%02d", creationTime.Day())
|
||||
eventFilename := fmt.Sprintf("%02d%02d%02d%09d_%s", creationTime.Hour(), creationTime.Minute(), creationTime.Second(), creationTime.Nanosecond(), typeId)
|
||||
return path.Join(me.storagePath, yearMonth, day, eventFilename)
|
||||
}
|
||||
|
||||
type IndexEntry struct {
|
||||
streamId uuid.UUID
|
||||
creationTime time.Time
|
||||
typeId string
|
||||
}
|
||||
|
||||
func appendIndex(filename string, entry *IndexEntry) error {
|
||||
indexFile, err := os.OpenFile(filename, os.O_APPEND | os.O_WRONLY | os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer indexFile.Close()
|
||||
|
||||
written, err := indexFile.Write(entry.streamId.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if written != 16 {
|
||||
return errors.New(fmt.Sprintf("Write error. Expected to write %v bytes, wrote only %v.", 16, written))
|
||||
}
|
||||
|
||||
creationTimeBytes, err := entry.creationTime.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeSizeAndBytes(indexFile, creationTimeBytes)
|
||||
writeSizeAndBytes(indexFile, []byte(entry.typeId))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readIndexNextEntry(f *os.File) (*IndexEntry, error) {
|
||||
index := IndexEntry{}
|
||||
|
||||
uuidBytes := make([]byte, 16)
|
||||
read, err := f.Read(uuidBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if read != 16 {
|
||||
return nil, errors.New(fmt.Sprintf("Integrity error. Expected to read %v bytes, got only %v bytes.", 16, read))
|
||||
}
|
||||
index.streamId = uuid.FromBytesOrNil(uuidBytes)
|
||||
|
||||
creationTimeBytes, err := readSizedBytes(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = index.creationTime.UnmarshalBinary(creationTimeBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
typeIdBytes, err := readSizedBytes(f)
|
||||
index.typeId = string(typeIdBytes)
|
||||
|
||||
return &index, nil;
|
||||
}
|
||||
|
||||
func writeEvent(filename string, data []byte) error {
|
||||
eventFile, err := os.OpenFile(filename, os.O_APPEND | os.O_WRONLY | os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer eventFile.Close()
|
||||
|
||||
eventFile.Write(data)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readEvent(filename string) ([]byte, error) {
|
||||
return ioutil.ReadFile(filename)
|
||||
}
|
||||
|
||||
func (me DailyDiskStorage) Write(event *StoredEvent) error {
|
||||
|
||||
eventFilename := me.getEventFilename(event.CreationTime, event.TypeId)
|
||||
os.MkdirAll(path.Dir(eventFilename), 0777)
|
||||
|
||||
err := writeEvent(eventFilename, event.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
index := &IndexEntry{event.StreamId, event.CreationTime, event.TypeId}
|
||||
|
||||
err = appendIndex(me.globalIndexFilename, index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = appendIndex(me.getStreamIndexFilename(event.StreamId), index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (me DailyDiskStorage) ReadStream(streamId uuid.UUID) ([]*StoredEvent, error) {
|
||||
|
||||
indexFile, err := os.OpenFile(me.getStreamIndexFilename(streamId), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer indexFile.Close()
|
||||
|
||||
events := make([]*StoredEvent, 0)
|
||||
for {
|
||||
indexEntry, err := readIndexNextEntry(indexFile)
|
||||
if err != nil && err.Error() == "EOF" {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := readEvent(me.getEventFilename(indexEntry.creationTime, indexEntry.typeId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event := &StoredEvent{streamId, indexEntry.creationTime, indexEntry.typeId, data}
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (me DailyDiskStorage) ReadAll() ([]*StoredEvent, error) {
|
||||
indexFile, err := os.OpenFile(me.globalIndexFilename, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer indexFile.Close()
|
||||
|
||||
events := make([]*StoredEvent, 0)
|
||||
for {
|
||||
indexEntry, err := readIndexNextEntry(indexFile)
|
||||
if err != nil && err.Error() == "EOF" {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := readEvent(me.getEventFilename(indexEntry.creationTime, indexEntry.typeId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
event := &StoredEvent{indexEntry.streamId, indexEntry.creationTime, indexEntry.typeId, data}
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
123
storage/dailydisk_test.go
Normal file
123
storage/dailydisk_test.go
Normal file
@ -0,0 +1,123 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"os"
|
||||
"path"
|
||||
"github.com/satori/go.uuid"
|
||||
"time"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func TestAddEvent(t *testing.T) {
|
||||
//Arrange
|
||||
storagePath := path.Join(os.TempDir(), uuid.NewV4().String())
|
||||
defer os.RemoveAll(storagePath)
|
||||
storage := NewDailyDiskStorage(storagePath)
|
||||
|
||||
aLocation, _ := time.LoadLocation("")
|
||||
aTime := time.Date(2016,2,11,9,53,32,1234567, aLocation)
|
||||
aggregateId := uuid.NewV4()
|
||||
aType := "myType"
|
||||
data := []byte("{}")
|
||||
|
||||
//Act
|
||||
err := storage.Write(&StoredEvent{aggregateId, aTime, aType, data})
|
||||
|
||||
//Assert
|
||||
if err != nil {
|
||||
t.Errorf("Write failed. Error: %v", err)
|
||||
}
|
||||
|
||||
readableDiskStorage := storage.(*DailyDiskStorage)
|
||||
|
||||
globalIndexFi, _ := os.Stat(readableDiskStorage.globalIndexFilename)
|
||||
if globalIndexFi == nil {
|
||||
t.Error("Write failed. Expected global index file, none exists.")
|
||||
}
|
||||
aggregateIndexFi, _ := os.Stat(readableDiskStorage.getStreamIndexFilename(aggregateId))
|
||||
if aggregateIndexFi == nil {
|
||||
t.Errorf("Write failed. Expected index for aggregate %v, none exists.", aggregateId.String())
|
||||
}
|
||||
eventFi, _ := os.Stat(readableDiskStorage.getEventFilename(aTime, aType))
|
||||
if eventFi == nil {
|
||||
t.Errorf("Write failed. Expected file for event %v, none exists.", aggregateId.String())
|
||||
}
|
||||
|
||||
//TODO: check indexes/event content
|
||||
}
|
||||
|
||||
func TestReadStream(t *testing.T) {
|
||||
//Arrange
|
||||
storagePath := path.Join(os.TempDir(), uuid.NewV4().String())
|
||||
defer os.RemoveAll(storagePath)
|
||||
storage := NewDailyDiskStorage(storagePath)
|
||||
|
||||
streamId := uuid.NewV4()
|
||||
ev1 := &StoredEvent{streamId, time.Now(), "1stType", []byte("1stEvent")}
|
||||
storage.Write(ev1)
|
||||
ev2 := &StoredEvent{streamId, time.Now(), "2ndType", []byte("2ndEvent")}
|
||||
storage.Write(ev2)
|
||||
|
||||
//Act
|
||||
storedEvents, err := storage.ReadStream(streamId)
|
||||
|
||||
//Assert
|
||||
if err != nil {
|
||||
t.Errorf("ReadStream failed. Error: %v", err)
|
||||
return
|
||||
}
|
||||
if len(storedEvents) != 2 {
|
||||
t.Errorf("ReadStream failed. Got %v stored events, expected %v", len(storedEvents), 2)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(storedEvents[0], ev1) {
|
||||
t.Errorf("ReadStream failed. First event doesn't match. %+v != %+v", storedEvents[0], ev1)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(storedEvents[1], ev2) {
|
||||
t.Errorf("ReadStream failed. Second event doesn't match. %+v != %+v", storedEvents[1], ev2)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAll(t *testing.T) {
|
||||
//Arrange
|
||||
storagePath := path.Join(os.TempDir(), uuid.NewV4().String())
|
||||
defer os.RemoveAll(storagePath)
|
||||
storage := NewDailyDiskStorage(storagePath)
|
||||
|
||||
stream1Id := uuid.NewV4()
|
||||
stream2Id := uuid.NewV4()
|
||||
ev1 := &StoredEvent{stream1Id, time.Now(), "1stType", []byte("1stEvent")}
|
||||
storage.Write(ev1)
|
||||
ev2 := &StoredEvent{stream2Id, time.Now(), "2ndType", []byte("2ndEvent")}
|
||||
storage.Write(ev2)
|
||||
ev3 := &StoredEvent{stream1Id, time.Now(), "3rdType", []byte("3rdEvent")}
|
||||
storage.Write(ev3)
|
||||
|
||||
//Act
|
||||
storedEvents, err := storage.ReadAll()
|
||||
|
||||
//Assert
|
||||
if err != nil {
|
||||
t.Errorf("ReadAll failed. Error: %v", err)
|
||||
return
|
||||
}
|
||||
if len(storedEvents) != 3 {
|
||||
t.Errorf("ReadAll failed. Got %v stored events, expected %v", len(storedEvents), 3)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(storedEvents[0], ev1) {
|
||||
t.Errorf("ReadAll failed. First event doesn't match. %+v != %+v", storedEvents[0], ev1)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(storedEvents[1], ev2) {
|
||||
t.Errorf("ReadAll failed. Second event doesn't match. %+v != %+v", storedEvents[1], ev2)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(storedEvents[2], ev3) {
|
||||
t.Errorf("ReadAll failed. Third event doesn't match. %+v != %+v", storedEvents[2], ev2)
|
||||
return
|
||||
}
|
||||
}
|
224
storage/simpledisk.go
Normal file
224
storage/simpledisk.go
Normal file
@ -0,0 +1,224 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"github.com/satori/go.uuid"
|
||||
"os"
|
||||
"time"
|
||||
"path"
|
||||
"fmt"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func NewSimpleDiskStorage(storagePath string) Storage {
|
||||
return &SimpleDiskStorage{storagePath, path.Join(storagePath, "eventindex")}
|
||||
}
|
||||
|
||||
type SimpleDiskStorage struct {
|
||||
storagePath string
|
||||
indexPath string
|
||||
}
|
||||
|
||||
func (me SimpleDiskStorage) getFilename(stream, extension string) string {
|
||||
return fmt.Sprintf("%v%v", path.Join(me.storagePath, stream[0:2], stream[2:]), extension)
|
||||
}
|
||||
|
||||
func (me SimpleDiskStorage) GetFilenameForEvents(stream string) string {
|
||||
return me.getFilename(stream, ".history")
|
||||
}
|
||||
|
||||
func writeSizeAndBytes(f *os.File, data []byte) (error) {
|
||||
sizeBytes := make([]byte, IntegerSizeInBytes)
|
||||
size := len(data)
|
||||
binary.BigEndian.PutUint64(sizeBytes, uint64(size))
|
||||
|
||||
written, err := f.Write(sizeBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if written != IntegerSizeInBytes {
|
||||
return errors.New(fmt.Sprintf("Write error. Expected to write %v bytes, wrote only %v.", IntegerSizeInBytes, written))
|
||||
}
|
||||
|
||||
written, err = f.Write(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if written != size {
|
||||
return errors.New(fmt.Sprintf("Write error. Expected to write %v bytes, wrote only %v.", size, written))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSizedBytes(f *os.File) ([]byte, error) {
|
||||
sizeBytes := make([]byte, IntegerSizeInBytes)
|
||||
read, err := f.Read(sizeBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if read != IntegerSizeInBytes {
|
||||
return nil, errors.New(fmt.Sprintf("Integrity error. Expected to read %d bytes, got %d bytes.", IntegerSizeInBytes, read))
|
||||
}
|
||||
size := binary.BigEndian.Uint64(sizeBytes)
|
||||
data := make([]byte, size)
|
||||
read, err = f.Read(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uint64(read) != size {
|
||||
return nil, errors.New(fmt.Sprintf("Integrity error. Expected to ready %d bytes, got %d bytes.", IntegerSizeInBytes, read))
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (me SimpleDiskStorage) Write(event *StoredEvent) error {
|
||||
filename := me.GetFilenameForEvents(event.StreamId.String())
|
||||
os.MkdirAll(path.Dir(filename), os.ModeDir)
|
||||
|
||||
indexFile, err := os.OpenFile(me.indexPath, os.O_APPEND | os.O_WRONLY | os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer indexFile.Close()
|
||||
|
||||
eventsFile, err := os.OpenFile(filename, os.O_APPEND | os.O_WRONLY | os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer eventsFile.Close()
|
||||
|
||||
stat, err := eventsFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
position := stat.Size()
|
||||
|
||||
creationTimeBytes, err := event.CreationTime.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeSizeAndBytes(eventsFile, creationTimeBytes)
|
||||
writeSizeAndBytes(eventsFile, []byte(event.TypeId))
|
||||
writeSizeAndBytes(eventsFile, event.Data)
|
||||
|
||||
indexFile.Write(event.StreamId.Bytes())
|
||||
positionBytes := make([]byte, IntegerSizeInBytes)
|
||||
binary.BigEndian.PutUint64(positionBytes, uint64(position))
|
||||
indexFile.Write(positionBytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (me SimpleDiskStorage) ReadStream(streamId uuid.UUID) ([]*StoredEvent, error) {
|
||||
streamName := streamId.String()
|
||||
offset := int64(0) //TODO snapshots
|
||||
filename := me.GetFilenameForEvents(streamName)
|
||||
|
||||
eventsFile, err := os.OpenFile(filename, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer eventsFile.Close()
|
||||
|
||||
eventsFile.Seek(offset, 0)
|
||||
|
||||
results := make([]*StoredEvent, 0)
|
||||
for {
|
||||
creationTime, typeId, data, err := getStoredData(eventsFile)
|
||||
if err != nil && err.Error() == "EOF" {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event := &StoredEvent{streamId, creationTime, typeId, data}
|
||||
results = append(results, event)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (me SimpleDiskStorage) ReadAll() ([]*StoredEvent, error) {
|
||||
indexFile, err := os.OpenFile(me.indexPath, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer indexFile.Close()
|
||||
|
||||
results := make([]*StoredEvent, 0)
|
||||
guidBytes := make([]byte, 16)
|
||||
offsetBytes := make([]byte, IntegerSizeInBytes)
|
||||
for {
|
||||
read, err := indexFile.Read(guidBytes)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if read != 16 {
|
||||
return nil, errors.New("index integrity error")
|
||||
}
|
||||
read, err = indexFile.Read(offsetBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if read != IntegerSizeInBytes {
|
||||
return nil, errors.New("index integrity error")
|
||||
}
|
||||
aggregateId, err := uuid.FromBytes(guidBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := binary.BigEndian.Uint64(offsetBytes)
|
||||
|
||||
storedEvent, err := me.retrieveStoredEvent(aggregateId, int64(offset))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, storedEvent)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (me SimpleDiskStorage) retrieveStoredEvent(streamId uuid.UUID, offset int64) (*StoredEvent, error) {
|
||||
filename := me.GetFilenameForEvents(streamId.String())
|
||||
|
||||
eventsFile, err := os.OpenFile(filename, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer eventsFile.Close()
|
||||
|
||||
eventsFile.Seek(offset, 0)
|
||||
|
||||
creationTime, typeId, data, err := getStoredData(eventsFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event := &StoredEvent{streamId, creationTime, typeId, data}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func getStoredData(eventsFile *os.File) (creationTime time.Time, typeId string, data []byte, err error) {
|
||||
creationTimeBytes, err := readSizedBytes(eventsFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = creationTime.UnmarshalBinary(creationTimeBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
typeIdBytes, err := readSizedBytes(eventsFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
typeId = string(typeIdBytes)
|
||||
|
||||
data, err = readSizedBytes(eventsFile)
|
||||
|
||||
return
|
||||
}
|
||||
|
23
storage/storage.go
Normal file
23
storage/storage.go
Normal file
@ -0,0 +1,23 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"github.com/satori/go.uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
const IntegerSizeInBytes = 8
|
||||
const StreamStartingCapacity = 512
|
||||
|
||||
type StoredEvent struct {
|
||||
StreamId uuid.UUID
|
||||
CreationTime time.Time
|
||||
TypeId string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
//TODO: performance - change reads array for some kind of iterator
|
||||
type Storage interface {
|
||||
Write(event *StoredEvent) error
|
||||
ReadStream(streamId uuid.UUID) ([]*StoredEvent, error)
|
||||
ReadAll() ([]*StoredEvent, error)
|
||||
}
|
Reference in New Issue
Block a user