forked from voidDB/voidDB
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvoid.go
271 lines (227 loc) · 6.25 KB
/
void.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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
package voidDB
import (
"errors"
"os"
"syscall"
"github.com/voidDB/voidDB/common"
"github.com/voidDB/voidDB/node"
"github.com/voidDB/voidDB/reader"
)
// A Void is a handle on a database. To interact with the database, enter a
// transaction through [*Void.BeginTxn].
type Void struct {
file *os.File
mmap []byte
}
// NewVoid creates and initialises a database file and its reader table at path
// and path.readers respectively, and returns a handle on the database, or
// [os.ErrExist] if a file already exists at path. See also [OpenVoid] for an
// explanation of the capacity parameter.
func NewVoid(path string, capacity int) (void *Void, e error) {
var (
file *os.File
)
_, e = os.Stat(path)
if e == nil {
return nil, os.ErrExist
}
file, e = os.Create(path)
if e != nil {
return
}
defer file.Close()
_, e = file.Write(
newMetaInit(),
)
if e != nil {
return
}
_, e = file.Write(
newMetaInit(),
)
if e != nil {
return
}
_, e = file.Write(
node.NewNode(),
)
if e != nil {
return
}
e = reader.NewReaderTable(path)
if e != nil {
return
}
return OpenVoid(path, capacity)
}
// OpenVoid returns a handle on the database persisted to the file at path.
//
// The capacity argument sets a hard limit on the size of the database file in
// number of bytes, but it applies only to transactions entered into via the
// database handle returned. The database file never shrinks, but it will not
// be allowed to grow if its size already exceeds capacity as at the time of
// invocation. A transaction running against the limit would incur
// [common.ErrorFull] on commit.
func OpenVoid(path string, capacity int) (void *Void, e error) {
var (
stat os.FileInfo
)
void = new(Void)
void.file, e = os.OpenFile(path, os.O_RDWR, 0)
if e != nil {
return
}
stat, e = void.file.Stat()
if e != nil {
return
}
if int(stat.Size()) > capacity {
capacity = int(stat.Size())
}
void.mmap, e = syscall.Mmap(
int(void.file.Fd()),
0,
capacity,
syscall.PROT_READ,
syscall.MAP_PRIVATE,
)
if e != nil {
return
}
return
}
// View is similar to [*Void.Update], except that it begins and passes to
// operation a read-only transaction. If operation results in a non-nil error,
// that error is [errors.Join]-ed with the result of *Txn.Abort; otherwise only
// the latter is returned.
func (void *Void) View(operation func(*Txn) error) (e error) {
var (
txn *Txn
)
txn, e = void.BeginTxn(true, false)
if e != nil {
return
}
e = operation(txn)
if e != nil {
return errors.Join(e,
txn.Abort(),
)
}
return txn.Abort()
}
// Update is a convenient wrapper around [*Void.BeginTxn], [*Txn.Commit], and
// [*Txn.Abort], to help applications ensure timely termination of writers.
// If operation is successful (in that it returns a nil error), the transaction
// is automatically committed and the result of [*Txn.Commit] is returned.
// Otherwise, the transaction is aborted and the output of [errors.Join]
// wrapping the return values of operation and [*Txn.Abort] is returned.
// IMPORTANT: See also [*Void.BeginTxn] for an explanation of the mustSync
// parameter.
func (void *Void) Update(mustSync bool, operation func(*Txn) error) (e error) {
var (
txn *Txn
)
txn, e = void.BeginTxn(false, mustSync)
if e != nil {
return
}
e = operation(txn)
if e != nil {
return errors.Join(e,
txn.Abort(),
)
}
return txn.Commit()
}
// BeginTxn begins a new transaction. The resulting transaction cannot modify
// data if readonly is true: any changes made are isolated to the transaction
// and non-durable; otherwise it is a write transaction. Since there cannot be
// more than one ongoing write transaction per database at any point in time,
// the function may return [syscall.EAGAIN] or [syscall.EWOULDBLOCK] (same
// error, “resource temporarily unavailable”) if an uncommitted/unaborted
// incumbent is present in any thread/process in the system.
//
// Setting mustSync to true ensures that all changes to data are flushed to
// disk when the transaction is committed, at a cost to write performance;
// setting it to false empowers the filesystem to optimise writes at a risk of
// data loss in the event of a crash at the level of the operating system or
// lower, e.g. hardware or power failure. Database corruption is also
// conceivable, albeit only if the filesystem does not preserve write order.
// TL;DR: set mustSync to true if safety matters more than speed; false if vice
// versa.
//
// BeginTxn returns [common.ErrorResized] if the database file has grown beyond
// the capacity initially passed to [OpenVoid]. This can happen if another
// database handle with a higher capacity has been obtained via a separate
// invocation of OpenVoid in the meantime. To adapt to the new size and
// proceed, close the database handle and replace it with a new invocation of
// OpenVoid.
func (void *Void) BeginTxn(readonly, mustSync bool) (txn *Txn, e error) {
var (
stat os.FileInfo
sync syncFunc
write writeFunc
)
stat, e = void.file.Stat()
if e != nil {
return
}
if int(stat.Size()) > cap(void.mmap) {
return nil, common.ErrorResized
}
if !readonly {
write = void.write
if mustSync {
sync = void.file.Sync
}
}
txn, e = newTxn(
void.file.Name(),
void.read,
write,
sync,
)
if e != nil {
return
}
return
}
// Close closes the database file and releases the corresponding memory map,
// rendering both unusable to any remaining transactions already entered into
// using the database handle. These stranded transactions could give rise to
// undefined behaviour if their use is attempted, which could disrupt the
// application, but in any case they pose no danger whatsoever to the data
// safely jettisoned.
func (void *Void) Close() (e error) {
e = errors.Join(
void.file.Close(),
syscall.Munmap(void.mmap),
)
*void = Void{}
return
}
func (void *Void) read(offset, length int) []byte {
return void.mmap[offset : offset+length]
}
func (void *Void) write(data []byte, offset int) (e error) {
if offset+len(data) > cap(void.mmap) {
return common.ErrorFull
}
_, e = void.file.WriteAt(data,
int64(offset),
)
if e != nil {
return
}
return
}
func align(size int) int {
return 1 << logarithm(size)
}
func logarithm(size int) (exp int) {
for exp = 12; 1<<exp < size; exp++ {
continue
}
return
}