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
|
package store
import (
"errors"
"fmt"
"iter"
"os"
"strconv"
"sync"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/lockedfile"
)
// EntryHandle is a handle on a state entry retrieved from a [Handle].
// Must only be used while its parent [Handle.Lock] is held.
type EntryHandle struct {
// Error returned while decoding pathname.
// A non-nil value disables EntryHandle.
DecodeErr error
// Checked pathname to entry file.
Pathname *check.Absolute
hst.ID
}
// open opens the underlying state entry file.
// A non-nil error returned by open is of type [hst.AppError].
func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
if eh.DecodeErr != nil {
return nil, eh.DecodeErr
}
if f, err := os.OpenFile(eh.Pathname.String(), flag, perm); err != nil {
return nil, &hst.AppError{Step: "open state entry", Err: err}
} else {
return f, nil
}
}
// Destroy removes the underlying state entry.
// A non-nil error returned by Destroy is of type [hst.AppError].
func (eh *EntryHandle) Destroy() error {
// destroy does not go through open
if eh.DecodeErr != nil {
return eh.DecodeErr
}
if err := os.Remove(eh.Pathname.String()); err != nil {
return &hst.AppError{Step: "destroy state entry", Err: err}
}
return nil
}
// save encodes [hst.State] and writes it to the underlying file.
// An error is returned if a file already exists with the same identifier.
// save does not validate the embedded [hst.Config].
// A non-nil error returned by save is of type [hst.AppError].
func (eh *EntryHandle) save(state *hst.State) error {
f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return err
}
err = entryEncode(f, state)
if closeErr := f.Close(); closeErr != nil && err == nil {
err = &hst.AppError{Step: "close state file", Err: closeErr}
}
return err
}
// Load loads and validates the state entry header, and returns the [hst.Enablement] byte.
// for a non-nil v, the full state payload is decoded and stored in the value pointed to by v.
// Load validates the embedded [hst.Config] value.
// A non-nil error returned by Load is of type [hst.AppError].
func (eh *EntryHandle) Load(v *hst.State) (hst.Enablement, error) {
f, err := eh.open(os.O_RDONLY, 0)
if err != nil {
return 0, err
}
var et hst.Enablement
if v != nil {
et, err = entryDecode(f, v)
if err == nil && v.ID != eh.ID {
err = &hst.AppError{Step: "validate state identifier", Err: os.ErrInvalid,
Msg: fmt.Sprintf("state entry %s has unexpected id %s", eh.ID.String(), v.ID.String())}
}
} else {
et, err = entryDecodeHeader(f)
}
if closeErr := f.Close(); closeErr != nil && err == nil {
err = &hst.AppError{Step: "close state file", Err: closeErr}
}
return et, err
}
// Handle is a handle on a [Store] segment.
// Initialised by [Store.Handle].
type Handle struct {
// Identity of instances tracked by this segment.
Identity int
// Pathname of directory that the segment referred to by Handle is rooted in.
Path *check.Absolute
// Inter-process mutex to synchronise operations against resources in this segment.
// Must not be held directly, callers should use [Handle.Lock] instead.
fileMu *lockedfile.Mutex
// Must be held alongside fileMu.
mu sync.Mutex
}
// Lock attempts to acquire a lock on [Handle].
// If successful, Lock returns a non-nil unlock function.
// A non-nil error returned by Lock is of type [hst.AppError].
func (h *Handle) Lock() (unlock func(), err error) {
if unlock, err = h.fileMu.Lock(); err != nil {
return nil, &hst.AppError{Step: "acquire lock on store segment " + strconv.Itoa(h.Identity), Err: err}
}
return
}
// Save attempts to save [hst.State] as a segment entry, and returns its [EntryHandle].
// Must be called while holding [Handle.Lock].
// An error is returned if an entry already exists with the same identifier.
// Save does not validate the embedded [hst.Config].
// A non-nil error returned by Save is of type [hst.AppError].
func (h *Handle) Save(state *hst.State) (*EntryHandle, error) {
eh := EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID}
return &eh, eh.save(state)
}
// Entries returns an iterator over all [EntryHandle] held in this segment.
// Must be called while holding [Handle.Lock].
// A non-nil error attached to a [EntryHandle] indicates a malformed identifier and is of type [hst.AppError].
// A non-nil error returned by Entries is of type [hst.AppError].
func (h *Handle) Entries() (iter.Seq[*EntryHandle], int, error) {
// for error reporting
const step = "read store segment entries"
// read directory contents, should only contain storeMutexName and identifier
var entries []os.DirEntry
if pl, err := os.ReadDir(h.Path.String()); err != nil {
return nil, -1, &hst.AppError{Step: step, Err: err}
} else {
entries = pl
}
// expects lock file
l := len(entries)
if l > 0 {
l--
}
return func(yield func(*EntryHandle) bool) {
for _, ent := range entries {
var eh = EntryHandle{Pathname: h.Path.Append(ent.Name())}
// this should never happen
if ent.IsDir() {
eh.DecodeErr = &hst.AppError{Step: step,
Err: errors.New("unexpected directory " + strconv.Quote(ent.Name()) + " in store")}
goto out
}
// silently skip lock file
if ent.Name() == MutexName {
continue
}
// this either indicates a serious bug or external interference
if err := eh.ID.UnmarshalText([]byte(ent.Name())); err != nil {
eh.DecodeErr = &hst.AppError{Step: "decode store segment entry", Err: err}
goto out
}
out:
if !yield(&eh) {
break
}
}
}, l, nil
}
// newHandle returns the address of a new segment [Handle] rooted in base.
func newHandle(base *check.Absolute, identity int) *Handle {
h := Handle{Identity: identity, Path: base.Append(strconv.Itoa(identity))}
h.fileMu = lockedfile.MutexAt(h.Path.Append(MutexName).String())
return &h
}
|