-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.go
383 lines (314 loc) · 11.1 KB
/
index.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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
package db
import (
"fmt"
"bytes"
"github.com/boltdb/bolt"
"github.com/pkg/errors"
)
const indiceBucket string = "indices"
// getLeagueIndexBucketRO returns the bucket corresponding
// to a specific league's index. This will never write
// and can be used safely with a readonly transaction.
//
// Will either panic or return a valid bucket.
func getLeagueIndexBucket(league LeagueHeapID, tx *bolt.Tx) *bolt.Bucket {
// Grab league bucket
leagueBucket := getLeagueBucket(league, tx)
// This can never fail, its a guarantee that the itemStoreBucket was registered
// and will always appear on a valid leagueBucket
indices := leagueBucket.Bucket([]byte(indiceBucket))
if indices == nil {
panic(fmt.Sprintf("%s bucket not found when expected", indiceBucket))
}
return indices
}
// getItemModIndexBucket returns a bucket which a given mod can be put
// when considering the item containing it.
//
// This WILL write if a bucket is not found. Hence, readonly tx unsafe.
func getItemModIndexBucket(rootType, rootFlavor, mod StringHeapID,
league LeagueHeapID, tx *bolt.Tx) (*bolt.Bucket, error) {
var err error
// Start at the index bucket
indexBucket := getLeagueIndexBucket(league, tx)
rootTypeBucket := indexBucket.Bucket(rootType.ToBytes())
if rootTypeBucket == nil {
rootTypeBucket, err = indexBucket.CreateBucket(rootType.ToBytes())
if err != nil {
return nil, errors.Wrapf(err, "failed to add index rootType bucket")
}
}
rootFlavorBucket := rootTypeBucket.Bucket(rootFlavor.ToBytes())
if rootFlavorBucket == nil {
rootFlavorBucket, err = rootTypeBucket.CreateBucket(rootFlavor.ToBytes())
if err != nil {
return nil, errors.Wrapf(err, "failed to add index rootFlavor bucket")
}
}
modBucket := rootFlavorBucket.Bucket(mod.ToBytes())
if modBucket == nil {
modBucket, err = rootFlavorBucket.CreateBucket(mod.ToBytes())
if err != nil {
return nil, errors.Wrapf(err, "failed to add index mod bucket")
}
}
// If we made it through, our currentBucket should be the one we want
return modBucket, nil
}
// getItemModIndexBucketRO returns a bucket which a given mod can be put
// when considering the item containing it.
//
// This WILL NOT write if a bucket is not found. Hence, readonly tx unsafe.
func getItemModIndexBucketRO(rootType, rootFlavor, mod StringHeapID,
league LeagueHeapID, tx *bolt.Tx) (*bolt.Bucket, error) {
// Start at the index bucket
indexBucket := getLeagueIndexBucket(league, tx)
rootTypeBucket := indexBucket.Bucket(rootType.ToBytes())
if rootTypeBucket == nil {
return nil, errors.New("invalid rootType, no bucket found")
}
rootFlavorBucket := rootTypeBucket.Bucket(rootFlavor.ToBytes())
if rootFlavorBucket == nil {
return nil, errors.New("invalid rootFlavor, no bucket found")
}
modBucket := rootFlavorBucket.Bucket(mod.ToBytes())
if modBucket == nil {
return nil, errors.New("invalid mod, no bucket found")
}
// If we made it through, our currentBucket should be the one we want
return modBucket, nil
}
// ModIndexKeySuffixLength allows us to fetch variable numbers
// of pre-pended values given their length.
const ModIndexKeySuffixLength = TimestampSize
// encodeModIndexKey generates a mod key based off of the provided data
//
// The mod index key is generated as [mod.Values..., now, updateSequence]
func encodeModIndexKey(mod ItemMod, now Timestamp) []byte {
// Pre-allocate index key so the entire key can be
// encoded with a single allocation.
modsLength := 2
indexKey := make([]byte, ModIndexKeySuffixLength+modsLength)
// Generate the suffix
suffix := (indexKey[modsLength:])[:0] // Deal with pre-allocated space
suffix = append(suffix, now.TruncateToIndexBucket()[:]...)
if len(suffix) != ModIndexKeySuffixLength {
panic(fmt.Sprintf("unexpected suffix length, got %d, expected %d",
len(suffix), ModIndexKeySuffixLength))
}
// Fill in the index from the front
//
// TODO: avoid appends, pre-size the backing slice to accomodate the
// contents including the header
index := indexKey[:0] // Deal with pre-allocated space
index = append(index, i16tob(mod.Value)...)
// And return the index with its suffix
return append(index, suffix...)
}
// decodeModIndexKey decodes a provided mod index key
//
// This returns the values encoded in the key.
//
// This is possible as the suffix is a fixed length and format while
// the values of the modifer are simple appended
func decodeModIndexKey(key []byte) ([]uint16, error) {
// Basic sanity check
if len(key) < ModIndexKeySuffixLength {
return nil, errors.New("invalid index key passed, less than length of suffix")
}
// Ensure we are divisible by 2 following the removal of the suffix
if (len(key)-ModIndexKeySuffixLength)%2 != 0 {
return nil, errors.New("invalid index key passed, values malformed")
}
valueBytes := key[:len(key)-ModIndexKeySuffixLength]
values := make([]uint16, len(valueBytes)/2)
for index := 0; index*2 < len(valueBytes); index++ {
values[index] = btoi16(valueBytes[index*2:])
}
return values, nil
}
// IndexItems adds tbe given items to their correct indices
// for efficient lookup. Returns number of index entries added.
//
// Provided items CAN differ in their league.
func IndexItems(items []Item, tx *bolt.Tx) (int, error) {
// Sanity check passed in transaction, better to do this than panic.
if !tx.Writable() {
return 0, errors.New("cannot IndexItems on readonly transaction")
}
// Silently exit when no items present to add
if len(items) < 1 {
return 0, nil
}
var added int
for _, item := range items {
for _, mod := range item.Mods {
// Grab the bucket we can actually insert things into
itemModBucket, err := getItemModIndexBucket(item.RootType, item.RootFlavor,
mod.Mod, item.League, tx)
if err != nil {
return 0, errors.New("failed to get item mod bucket")
}
modKey := encodeModIndexKey(mod, item.When)
// Check for pre-existing items in the bucket, if none, we establish
// the bucket
existing := itemModBucket.Get(modKey)
wrapped := IndexEntry(existing)
wrapped = IndexEntryAppend(wrapped, item.ID)
itemModBucket.Put(modKey, wrapped)
added++
}
}
return added, nil
}
// DeindexItems removes tbe given items from their correct indices
//
// If an index entry cannot be removed, we return an error. This ensures
// all existing index entries must be alive
func DeindexItems(items []Item, tx *bolt.Tx) error {
// Sanity check passed in transaction, better to do this than panic.
if !tx.Writable() {
return errors.New("cannot IndexItems on readonly transaction")
}
// Silently exit when no items present to add
if len(items) < 1 {
return nil
}
for _, item := range items {
for _, mod := range item.Mods {
// Grab the bucket we can actually insert things into
itemModBucket, err := getItemModIndexBucket(item.RootType, item.RootFlavor,
mod.Mod, item.League, tx)
if err != nil {
return errors.New("failed to get item mod bucket")
}
modKey := encodeModIndexKey(mod, item.When)
existing := itemModBucket.Get(modKey)
wrapped := IndexEntry(existing)
wrapped = IndexEntryRemove(wrapped, item.ID)
if wrapped == nil {
// Nothing else resides at this index
itemModBucket.Delete(modKey)
} else {
// Add back with removed data
itemModBucket.Put(modKey, wrapped)
}
}
}
return nil
}
// IndexEntry represents bytes interpreted as an entry within the index
//
// Whenever possible, we avoid allocations.
type IndexEntry []byte
// IndexEntryAppend adds another ID to the entry
//
// If an id is already present in the id, we end up with a duplicate.
// Such is life.
func IndexEntryAppend(entry IndexEntry, id ID) IndexEntry {
var result []byte
if entry == nil {
// Copy necessary due to boltdb semantics for passed buffers
result = make([]byte, len(id))
copy(result, id[:])
} else {
// We assume item not already present in bucket.
// If it is, we end up with a duplicate.
//
// Allocate a buffer large enough for an append
// without another allocation.
// Yes, this looks super dirty. TODO: cleanup D:
result = make([]byte, len(entry)+IDSize)[:0]
result = append(result, entry...)
result = append(result, id[:]...)
}
return IndexEntry(result)
}
// IndexEntryRemove removes a given ID from the entry
//
// If the ID is the last of the entry, the backing slice is set
// to nil. In that case, its the callers responsibility to ensure they
// Unwrap a valid byte slice.
func IndexEntryRemove(entry IndexEntry, id ID) IndexEntry {
// If the backing array is nil, then we can't remove an ID
// and the database is inconsistent.
if entry == nil {
panic(fmt.Sprintf("attempted to remove ID from nil IndexEntry, id=%v",
id))
}
// For removal, we Stride over the array in IDSize increments
// and call Equal to determine which index we remove.
index := -1 // Index is in terms of IDSize increments
for i := 0; i < len(entry); i += IDSize {
equal := bytes.Equal(id[:], entry[i:i+IDSize])
if equal {
index = i
break
}
}
// If we can't find the ID, invalid index state, so panic.
if index == -1 {
panic(fmt.Sprintf("attempted to remove non-existent ID, id=%v", id))
}
// Check if this is the last entry, if yes, then easy nil.
if len(entry) == IDSize {
return nil
}
// Remove the entry using fewest allocations possible.
//
// We have to asssume our internal buffer for the entry came from
// bolt, hence the new buffer to mutate.
removed := make([]byte, len(entry)-IDSize)[:0]
removed = append(removed, entry[:index]...)
removed = append(removed, entry[index+IDSize:]...)
return removed
}
// ForEachID calls the provided callback with each id contained
// within the IndexEntry. Order of returned IDs is in Append-descending
func (entry IndexEntry) ForEachID(cb func(id ID)) {
var id ID
for i := 0; i < len(entry); i += IDSize {
copy(id[:], entry[i:i+IDSize])
cb(id)
}
}
// GetIDs returns all IDs in the entry.
//
// Provided array slice will be resized if necessary or a new one
// will be created if passed nil. Updated slice will be returned.
func (entry IndexEntry) GetIDs(ids []ID) []ID {
idCount := len(entry) / IDSize
if ids == nil || cap(ids) < idCount {
ids = make([]ID, idCount)
}
ids = ids[:0]
for i := 0; i < len(entry); i += IDSize {
var id ID
copy(id[:], entry[i:i+IDSize])
ids = append(ids, id)
}
return ids
}
// IndexEntryCount returns the number of index entries across all leagues
func IndexEntryCount(db *bolt.DB) (int, error) {
var count int
leagueStrings, err := ListLeagues(db)
if err != nil {
return 0, err
}
leagueIDs, err := GetLeagues(leagueStrings, db)
if err != nil {
return 0, err
}
return count, db.View(func(tx *bolt.Tx) error {
for _, id := range leagueIDs {
b := getLeagueIndexBucket(id, tx)
if b == nil {
return errors.Errorf("%s bucket not found", itemStoreBucket)
}
stats := b.Stats()
count += stats.KeyN
}
return nil
})
}