Skip to content

Commit 217d4d2

Browse files
committed
Massive, likely breaking (!): Implement test suite, various Keeper fixes/refactoring
1 parent e41e182 commit 217d4d2

File tree

19 files changed

+566
-140
lines changed

19 files changed

+566
-140
lines changed

.github/workflows/go.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ jobs:
1313
with:
1414
go-version: '1.22'
1515
- name: Water's fine
16-
run: go test -race -v ./...
16+
run: go database_test -race -v ./...
1717
- name: Run coverage
18-
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
18+
run: go database_test -race -coverprofile=coverage.txt -covermode=atomic ./...
1919
- name: Upload coverage to Codecov
2020
run: bash <(curl -s https://codecov.io/bash)

bitcask/bitcask.go

Lines changed: 130 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import (
88
"path/filepath"
99
"strings"
1010
"sync"
11+
"sync/atomic"
1112

1213
"git.tcp.direct/Mirrors/bitcask-mirror"
1314

1415
"git.tcp.direct/tcp.direct/database"
16+
"git.tcp.direct/tcp.direct/database/kv"
1517
"git.tcp.direct/tcp.direct/database/metadata"
1618
"git.tcp.direct/tcp.direct/database/models"
1719
)
@@ -20,23 +22,43 @@ import (
2022
type Store struct {
2123
*bitcask.Bitcask
2224
database.Searcher
23-
closed bool
25+
closed *atomic.Bool
26+
}
27+
28+
// Get is a wrapper around the bitcask Get function for error regularization.
29+
func (s *Store) Get(key []byte) ([]byte, error) {
30+
if s.closed.Load() {
31+
return nil, fs.ErrClosed
32+
}
33+
ret, err := s.Bitcask.Get(key)
34+
err = kv.RegularizeKVError(key, ret, err)
35+
return ret, err
36+
}
37+
38+
// Close is a wrapper around the bitcask Close function.
39+
func (s *Store) Close() error {
40+
if s.closed.Load() {
41+
return fs.ErrClosed
42+
}
43+
s.closed.Store(true)
44+
return s.Bitcask.Close()
2445
}
2546

2647
// Backend returns the underlying bitcask instance.
27-
func (s Store) Backend() any {
48+
func (s *Store) Backend() any {
2849
return s.Bitcask
2950
}
3051

3152
// DB is a mapper of a Filer and Searcher implementation using Bitcask.
3253
type DB struct {
33-
store map[string]Store
54+
store map[string]*Store
3455
path string
3556
mu *sync.RWMutex
3657
meta *metadata.Metadata
37-
initialized bool
58+
initialized *atomic.Bool
3859
}
3960

61+
// Meta returns the [models.Metadata] implementation of the bitcask keeper.
4062
func (db *DB) Meta() models.Metadata {
4163
db.mu.RLock()
4264
m := db.meta
@@ -60,12 +82,9 @@ func (db *DB) AllStores() map[string]database.Filer {
6082

6183
func (db *DB) init() error {
6284
var err error
63-
db.mu.RLock()
64-
if db.initialized {
65-
db.mu.RUnlock()
85+
if db.initialized.Load() {
6686
return nil
6787
}
68-
db.mu.RUnlock()
6988
db.mu.Lock()
7089
defer db.mu.Unlock()
7190
if _, err = os.Stat(db.path); os.IsNotExist(err) {
@@ -86,7 +105,7 @@ func (db *DB) init() error {
86105
if db.meta.Type() != db.Type() {
87106
return fmt.Errorf("meta.json is not a bitcask meta file")
88107
}
89-
db.initialized = true
108+
db.initialized.Store(true)
90109
return nil
91110
}
92111

@@ -95,7 +114,7 @@ func (db *DB) init() error {
95114
if err != nil {
96115
return fmt.Errorf("error creating meta file: %w", err)
97116
}
98-
db.initialized = true
117+
db.initialized.Store(true)
99118
return nil
100119
}
101120

@@ -104,12 +123,14 @@ func (db *DB) init() error {
104123

105124
// OpenDB will either open an existing set of bitcask datastores at the given directory, or it will create a new one.
106125
func OpenDB(path string) *DB {
126+
ainit := &atomic.Bool{}
127+
ainit.Store(false)
107128
return &DB{
108-
store: make(map[string]Store),
129+
store: make(map[string]*Store),
109130
path: path,
110131
mu: &sync.RWMutex{},
111132
meta: nil,
112-
initialized: false,
133+
initialized: ainit,
113134
}
114135
}
115136

@@ -123,9 +144,8 @@ func (db *DB) Discover() ([]string, error) {
123144
stores := make([]string, 0)
124145
errs := make([]error, 0)
125146
if db.store == nil {
126-
db.store = make(map[string]Store)
147+
db.store = make(map[string]*Store)
127148
}
128-
os.Stat(filepath.Join(db.path, "meta.json"))
129149

130150
entries, err := fs.ReadDir(os.DirFS(db.path), ".")
131151
if err != nil {
@@ -157,17 +177,19 @@ func (db *DB) Discover() ([]string, error) {
157177
}
158178
println("WARN: bitcask store", name, "has bad metadata, attempting to repair")
159179
oldMeta := filepath.Join(db.path, name, "meta.json")
160-
newMeta := filepath.Join(db.path, name, "meta.json.backup")
161-
println("WARN: renaming", oldMeta, "to", newMeta)
162-
// likely defunct lockfile is present too, remove it
163-
if osErr := os.Rename(oldMeta, newMeta); osErr != nil {
164-
println("WARN: failed to rename", oldMeta, "to", newMeta, ":", osErr)
165-
return
180+
oldMetaBackup := filepath.Join(db.path, name, "meta.json.backup")
181+
println("WARN: renaming", oldMeta, "to", oldMetaBackup)
182+
if osErr := os.Rename(oldMeta, oldMetaBackup); osErr != nil {
183+
println("Fatal: failed to rename", oldMeta, "to", oldMetaBackup, ":", osErr)
184+
panic(osErr)
166185
}
186+
187+
// likely defunct lockfile is present too, remove it
167188
if _, serr := os.Stat(filepath.Join(db.path, name, "lock")); serr == nil {
168189
println("WARN: removing defunct lockfile")
169190
_ = os.Remove(filepath.Join(db.path, name, "lock"))
170191
}
192+
171193
retry = true
172194
})
173195
if retry {
@@ -176,7 +198,9 @@ func (db *DB) Discover() ([]string, error) {
176198
errs = append(errs, e)
177199
continue
178200
}
179-
db.store[name] = Store{Bitcask: c}
201+
aclosed := &atomic.Bool{}
202+
aclosed.Store(false)
203+
db.store[name] = &Store{Bitcask: c, closed: aclosed}
180204
stores = append(stores, name)
181205
}
182206

@@ -225,44 +249,82 @@ func (db *DB) Init(storeName string, opts ...any) error {
225249
}
226250
var bitcaskopts []bitcask.Option
227251
for _, opt := range opts {
228-
if _, ok := opt.(bitcask.Option); !ok {
229-
return errors.New("invalid bitcask option type")
252+
_, isOptOK := opt.(bitcask.Option)
253+
_, isOptsOK := opt.([]bitcask.Option)
254+
if !isOptOK && !isOptsOK {
255+
return fmt.Errorf("invalid bitcask option type (%T): %v", opt, opt)
256+
}
257+
if isOptOK {
258+
bitcaskopts = append(bitcaskopts, opt.(bitcask.Option))
259+
}
260+
if isOptsOK {
261+
bitcaskopts = append(bitcaskopts, opt.([]bitcask.Option)...)
230262
}
231-
bitcaskopts = append(bitcaskopts, opt.(bitcask.Option))
232263
}
233-
db.mu.Lock()
234-
defer db.mu.Unlock()
235264

236265
if len(defaultBitcaskOptions) > 0 {
237-
bitcaskopts = append(bitcaskopts, defaultBitcaskOptions...)
266+
bitcaskopts = append(defaultBitcaskOptions, bitcaskopts...)
238267
}
239268

269+
db.mu.Lock()
270+
err := db.initStore(storeName, bitcaskopts...)
271+
db.mu.Unlock()
272+
273+
return err
274+
}
275+
276+
// initStore is a helper function to initialize a bitcask store, caller must hold keeper's lock.
277+
func (db *DB) initStore(storeName string, opts ...bitcask.Option) error {
240278
if _, ok := db.store[storeName]; ok {
241279
return ErrStoreExists
242280
}
243-
path := db.Path()
244-
if !strings.HasSuffix(db.Path(), "/") {
245-
path = db.Path() + "/"
246-
}
247-
c, e := bitcask.Open(path+storeName, bitcaskopts...)
281+
282+
c, e := bitcask.Open(filepath.Join(db.Path(), storeName), opts...)
248283
if e != nil {
249284
return e
250285
}
251-
db.store[storeName] = Store{Bitcask: c}
286+
287+
aclosed := &atomic.Bool{}
288+
aclosed.Store(false)
289+
db.store[storeName] = &Store{Bitcask: c, closed: aclosed}
252290
return nil
253291
}
254292

293+
// Destroy will remove the bitcask store and all data associated with it.
294+
func (db *DB) Destroy(storeName string) error {
295+
db.mu.Lock()
296+
defer db.mu.Unlock()
297+
st, ok := db.store[storeName]
298+
if !ok {
299+
return ErrBogusStore
300+
}
301+
err := st.Close()
302+
if err != nil {
303+
return err
304+
}
305+
delete(db.store, storeName)
306+
return os.RemoveAll(filepath.Join(db.path, storeName))
307+
}
308+
255309
// With calls the given underlying bitcask instance.
256310
func (db *DB) With(storeName string) database.Store {
257311
if err := db.init(); err != nil {
258312
panic(err)
259313
}
260314
db.mu.RLock()
261-
defer db.mu.RUnlock()
262315
d, ok := db.store[storeName]
263-
if ok {
316+
if ok && !d.closed.Load() {
317+
db.mu.RUnlock()
264318
return d
265319
}
320+
if ok && d.closed.Load() {
321+
db.mu.RUnlock()
322+
db.mu.Lock()
323+
delete(db.store, storeName)
324+
db.mu.Unlock()
325+
return nil
326+
}
327+
db.mu.RUnlock()
266328
return nil
267329
}
268330

@@ -271,42 +333,54 @@ func (db *DB) WithNew(storeName string, opts ...any) database.Filer {
271333
if err := db.init(); err != nil {
272334
panic(err)
273335
}
274-
db.mu.RLock()
275-
defer db.mu.RUnlock()
336+
337+
newOpts := make([]bitcask.Option, 0)
276338
for _, opt := range opts {
277339
if _, ok := opt.(bitcask.Option); !ok {
278340
fmt.Println("invalid bitcask option type: ", opt)
279341
continue
280342
}
281-
defaultBitcaskOptions = append(defaultBitcaskOptions, opt.(bitcask.Option))
343+
newOpts = append(newOpts, opt.(bitcask.Option))
282344
}
345+
346+
db.mu.Lock()
347+
defer db.mu.Unlock()
348+
283349
d, ok := db.store[storeName]
284350
if ok {
351+
if d.Bitcask == nil || d.closed == nil || d.closed.Load() {
352+
delete(db.store, storeName)
353+
if err := db.initStore(storeName, newOpts...); err != nil {
354+
fmt.Println("error re-initializing bitcask store: ", err.Error())
355+
}
356+
return db.store[storeName]
357+
}
285358
return d
286359
}
287-
db.mu.RUnlock()
288-
err := db.Init(storeName)
289-
db.mu.RLock()
360+
361+
err := db.initStore(storeName, newOpts...)
290362
if err != nil {
291363
fmt.Println("error creating bitcask store: ", err)
292-
293364
}
294365
return db.store[storeName]
295366
}
296367

297368
// Close is a simple shim for bitcask's Close function.
298369
func (db *DB) Close(storeName string) error {
299-
db.mu.Lock()
300-
defer db.mu.Unlock()
370+
db.mu.RLock()
301371
st, ok := db.store[storeName]
302372
if !ok {
373+
db.mu.RUnlock()
303374
return ErrBogusStore
304375
}
376+
db.mu.RUnlock()
305377
err := st.Close()
306378
if err != nil {
307379
return err
308380
}
381+
db.mu.Lock()
309382
delete(db.store, storeName)
383+
db.mu.Unlock()
310384
return nil
311385
}
312386

@@ -334,7 +408,10 @@ const (
334408
// In the case of an error, withAll will continue and return a compound form of any errors that occurred.
335409
// For now this is just for Close and Sync, thusly it does a hard lock on the Keeper.
336410
func (db *DB) withAll(action withAllAction) error {
337-
if db == nil || db.store == nil || len(db.store) < 1 {
411+
if db == nil || db.store == nil {
412+
panic("bitcask: nil db or db.store")
413+
}
414+
if len(db.store) < 1 {
338415
return ErrNoStores
339416
}
340417
var errs = make([]error, len(db.store))
@@ -344,13 +421,19 @@ func (db *DB) withAll(action withAllAction) error {
344421
errs = append(errs, namedErr(name, ErrBogusStore))
345422
continue
346423
}
424+
if store.closed.Load() {
425+
continue
426+
}
347427
switch action {
348428
case dclose:
429+
db.mu.Lock()
349430
closeErr := store.Close()
350431
if errors.Is(closeErr, fs.ErrClosed) {
351432
continue
352433
}
353434
err = namedErr(name, closeErr)
435+
delete(db.store, name)
436+
db.mu.Unlock()
354437
case dsync:
355438
err = namedErr(name, store.Sync())
356439
default:
@@ -391,6 +474,8 @@ func (db *DB) SyncAll() error {
391474
return db.withAll(dsync)
392475
}
393476

477+
// Type returns the type of keeper, in this case "bitcask".
478+
// This is in order to implement [database.Keeper].
394479
func (db *DB) Type() string {
395480
return "bitcask"
396481
}

0 commit comments

Comments
 (0)