-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmemory.go
189 lines (161 loc) · 5.26 KB
/
memory.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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package file
import (
"encoding/json"
"errors"
"fmt"
"os"
"sort"
"github.com/go-joe/joe"
"go.uber.org/zap"
)
// memory is an implementation of a joe.Memory which stores all values as a JSON
// encoded file. Note that there is no need for a joe.Memory to handle
// synchronization for concurrent access (e.g. via locks) because this is
// automatically handled by the joe.Brain.
type memory struct {
path string
logger *zap.Logger
data map[string][]byte
}
// Memory is a joe.Option which is supposed to be passed to joe.New(…) to
// configure a new bot. The path indicates the destination file at which the
// memory will store its values encoded as JSON object. If there is already a
// JSON encoded file at the given path it will be loaded and decoded into memory
// to serve future requests. If the file exists but cannot be opened or does not
// contain a valid JSON object its error will be deferred until the bot is
// actually started via its Run() function.
//
// Example usage:
// b := joe.New("example",
// file.Memory("/tmp/joe.json"),
// …
// )
func Memory(path string) joe.Module {
return joe.ModuleFunc(func(conf *joe.Config) error {
memory, err := NewMemory(path, WithLogger(conf.Logger("memory")))
if err != nil {
return err
}
conf.SetMemory(memory)
return nil
})
}
// NewMemory creates a new Memory instance that persists all values to the given
// path. If there is already a JSON encoded file at the given path it is loaded
// and decoded into memory to serve future requests. An error is returned if the
// file exists but cannot be opened or does not contain a valid JSON object.
func NewMemory(path string, opts ...Option) (joe.Memory, error) {
memory := &memory{
path: path,
data: map[string][]byte{},
}
for _, opt := range opts {
err := opt(memory)
if err != nil {
return nil, err
}
}
if memory.logger == nil {
memory.logger = zap.NewNop()
}
memory.logger.Debug("Opening memory file", zap.String("path", path))
f, err := os.Open(path)
switch {
case os.IsNotExist(err):
memory.logger.Debug("File does not exist. Continuing with empty memory", zap.String("path", path))
case err != nil:
return nil, fmt.Errorf("failed to open file: %w", err)
default:
memory.logger.Debug("Decoding JSON from memory file", zap.String("path", path))
err := json.NewDecoder(f).Decode(&memory.data)
_ = f.Close()
if err != nil {
return nil, fmt.Errorf("failed decode data as JSON: %w", err)
}
}
memory.logger.Info("Memory initialized successfully",
zap.String("path", path),
zap.Int("num_memories", len(memory.data)),
)
return memory, nil
}
// Set assign the key to the value and then saves the updated memory to its JSON
// file. An error is returned if this function is called after the memory was
// closed already or if the file could not be written or updated.
func (m *memory) Set(key string, value []byte) error {
if m.data == nil {
return errors.New("brain was already shut down")
}
m.data[key] = value
return m.persist()
}
// Get returns the value that is associated with the given key. The second
// return value indicates if the key actually existed in the memory.
//
// An error is only returned if this function is called after the memory was
// closed already.
func (m *memory) Get(key string) ([]byte, bool, error) {
if m.data == nil {
return nil, false, errors.New("brain was already shut down")
}
value, ok := m.data[key]
return value, ok, nil
}
// Delete removes any value that might have been assigned to the key earlier.
// The boolean return value indicates if the memory contained the key. If it did
// not contain the key the function does nothing and returns without an error.
// If the key existed it is removed and the corresponding JSON file is updated.
//
// An error is returned if this function is called after the memory was closed
// already or if the file could not be written or updated.
func (m *memory) Delete(key string) (bool, error) {
if m.data == nil {
return false, errors.New("brain was already shut down")
}
_, ok := m.data[key]
if !ok {
return false, nil
}
delete(m.data, key)
return ok, m.persist()
}
// Keys returns a list of all keys known to this memory.
// An error is only returned if this function is called after the memory was
// closed already.
func (m *memory) Keys() ([]string, error) {
if m.data == nil {
return nil, errors.New("brain was already shut down")
}
keys := make([]string, 0, len(m.data))
for k := range m.data {
keys = append(keys, k)
}
// provide a stable result
sort.Strings(keys)
return keys, nil
}
// Close removes all data from the memory. Note that all calls to the memory
// will fail after this function has been called.
func (m *memory) Close() error {
if m.data == nil {
return errors.New("brain was already closed")
}
m.data = nil
return nil
}
func (m *memory) persist() error {
f, err := os.OpenFile(m.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
if err != nil {
return fmt.Errorf("failed to open file to persist data: %w", err)
}
err = json.NewEncoder(f).Encode(m.data)
if err != nil {
_ = f.Close()
return fmt.Errorf("failed to encode data as JSON: %w", err)
}
err = f.Close()
if err != nil {
return fmt.Errorf("failed to close file; data might not have been fully persisted to disk: %w", err)
}
return nil
}