-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathrambo.go
163 lines (137 loc) · 4.24 KB
/
rambo.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package rambo
import (
"encoding/gob"
"errors"
"io"
"os"
"path/filepath"
"sync"
)
// Rambo is an implementation of the Prevalent System design pattern, in which
// business objects are kept live in memory and transactions are journaled for
// system recovery. A prevalent system needs enough memory to hold its entire
// state in RAM (the "prevalent hypothesis").
//
// This version is heavily inspired on Prevayler's author Klaus Wuestefeld's
// "PrevaylerJr" version (https://bit.ly/3tGxw3P).
//
// The name "Rambo" comes from a book called "Java RAMBO Manifesto: RAM-based
// Objects, Prevayler, and Raw Speed" (https://amzn.to/3IbPsaC) by Peter Wayner.
type Rambo[System any] struct {
system *System
encoder *gob.Encoder
mutex sync.Mutex
}
// Load restores the system state from the disk and wraps the given system in a
// prevalent layer.
//
// The given initial system state is hydrated by loading the last persisted
// state from the file located at the given path and applying any journaled
// transactions. If no persisted state is found (very first time running the
// system), then given initial system state is used.
//
// IMPORTANT: don't forget to provide samples (empty struct pointers) of the
// commands as the last set of arguments. This is required for deserialization.
func Load[System any](storageFilePath string, initial *System, commandSamples ...any) (*Rambo[System], error) {
gob.Register(initial)
for _, commandSample := range commandSamples {
gob.Register(commandSample)
}
tempFilePath := storageFilePath + ".tmp"
filePath := tempFilePath
if _, err := os.Stat(storageFilePath); !errors.Is(err, os.ErrNotExist) {
filePath = storageFilePath
}
file, err := openFile(filePath)
if err != nil {
return nil, err
}
system, err := restoreSystem(initial, file)
if err != nil {
return nil, err
}
file, err = openFile(tempFilePath)
encoder := gob.NewEncoder(file)
if err != nil {
return nil, err
}
err = encoder.Encode(system)
if err != nil {
return nil, err
}
err = os.Rename(tempFilePath, storageFilePath)
if err != nil {
return nil, err
}
return &Rambo[System]{
system: system,
encoder: encoder,
}, nil
}
// Command describes a transactional system state change. Implementations of
// this interface are used with the Rambo.Transact method.
type Command[System any] interface {
ExecuteOn(*System) error
}
// Transact journals the given command and then executes it. Beware that the
// journaled commands will be replayed during bootstrap – during Load – so don't
// perform any third-party interactions from commands (e.g. sending emails o
// charging credit cards). Commands should be deterministic, meaning that they
// should always produce the same result when replayed.
func (p *Rambo[System]) Transact(command Command[System]) error {
p.mutex.Lock()
defer p.mutex.Unlock()
err := p.encoder.Encode(&command)
if err != nil {
return err
}
return command.ExecuteOn(p.system)
}
// Query describes a query to run against the system. Queries are not journaled.
// Implementations of this interface are used with Rambo.Query method.
type Query[System any] interface {
QueryOn(*System) any
}
// Query executes the given query against the system. The output from the query
// is returned and must be type-casted in order to be used.
//
// See also: Rambo.QueryFn
func (p *Rambo[System]) Query(query Query[System]) any {
return query.QueryOn(p.system)
}
// QueryFn executes the given query function against the system. The output from
// the query is returned and must be type-casted in order to be used.
//
// See also: Rambo.Query
func (p *Rambo[System]) QueryFn(fn func(*System) any) any {
return fn(p.system)
}
func restoreSystem[System any](system *System, file *os.File) (*System, error) {
decoder := gob.NewDecoder(file)
err := decoder.Decode(&system)
if err == io.EOF {
return system, nil
}
if err != nil {
return nil, err
}
for {
var command Command[System]
err := decoder.Decode(&command)
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
_ = command.ExecuteOn(system)
}
return system, nil
}
func openFile(path string) (*os.File, error) {
err := os.MkdirAll(filepath.Dir(path), 0770)
if err != nil {
return nil, err
}
return os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
}