certificate: improve gob migration

`storage.KeyValueStore` requires ordered iteration, which redis
doesn't support natively. This would require loading all the keys
into memory and then processing them, rather than iterating over them
one-by-one.

This adds a temporary `IterateUnordered` to handle the migrations
more gracefully.

Change-Id: I55b763500523077c7ab8fdfad175c32cc7788e47
This commit is contained in:
Egon Elbre 2023-01-31 17:37:21 +02:00 committed by Storj Robot
parent fa26ae85e9
commit b8c7dcbf7b
8 changed files with 78 additions and 4 deletions

View File

@ -300,9 +300,7 @@ func (authDB *DB) put(ctx context.Context, userID string, auths Group) (err erro
// MigrateGob migrates gob encoded Group to protobuf encoded Group. // MigrateGob migrates gob encoded Group to protobuf encoded Group.
func (authDB *DB) MigrateGob(ctx context.Context, progress func(userID string)) (err error) { func (authDB *DB) MigrateGob(ctx context.Context, progress func(userID string)) (err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)
err = authDB.db.Iterate(ctx, storage.IterateOptions{ err = authDB.db.IterateUnordered(ctx, func(ctx context.Context, it storage.Iterator) error {
Recurse: true,
}, func(ctx context.Context, it storage.Iterator) error {
var item storage.ListItem var item storage.ListItem
for it.Next(ctx, &item) { for it.Next(ctx, &item) {

View File

@ -35,7 +35,7 @@ func cmdMigrate(cmd *cobra.Command, args []string) error {
count := 0 count := 0
return authorizationDB.MigrateGob(ctx, func(userID string) { return authorizationDB.MigrateGob(ctx, func(userID string) {
if count%100 == 0 { if count%100 == 0 {
log.Info("progress", zap.String("user", userID), zap.Int("count", count)) log.Info("progress", zap.String("last", userID), zap.Int("total-processed-count", count))
} }
count++ count++
}) })

View File

@ -230,6 +230,13 @@ func (client *Client) Iterate(ctx context.Context, opts storage.IterateOptions,
return client.IterateWithoutLookupLimit(ctx, opts, fn) return client.IterateWithoutLookupLimit(ctx, opts, fn)
} }
// IterateUnordered iterates over all data, however, does not guarantee ordering.
// It only guarantees all items are iterated at least once.
func (client *Client) IterateUnordered(ctx context.Context, fn func(context.Context, storage.Iterator) error) (err error) {
defer mon.Task()(&ctx)(&err)
return client.Iterate(ctx, storage.IterateOptions{}, fn)
}
// IterateWithoutLookupLimit calls the callback with an iterator over the keys, but doesn't enforce default limit on opts. // IterateWithoutLookupLimit calls the callback with an iterator over the keys, but doesn't enforce default limit on opts.
func (client *Client) IterateWithoutLookupLimit(ctx context.Context, opts storage.IterateOptions, fn func(context.Context, storage.Iterator) error) (err error) { func (client *Client) IterateWithoutLookupLimit(ctx context.Context, opts storage.IterateOptions, fn func(context.Context, storage.Iterator) error) (err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)

View File

@ -72,6 +72,9 @@ type KeyValueStore interface {
List(ctx context.Context, start Key, limit int) (Keys, error) List(ctx context.Context, start Key, limit int) (Keys, error)
// Iterate iterates over items based on opts. // Iterate iterates over items based on opts.
Iterate(ctx context.Context, opts IterateOptions, fn func(context.Context, Iterator) error) error Iterate(ctx context.Context, opts IterateOptions, fn func(context.Context, Iterator) error) error
// IterateUnordered iterates over all data, however, does not guarantee ordering.
// It only guarantees all items are iterated at least once.
IterateUnordered(ctx context.Context, fn func(context.Context, Iterator) error) error
// IterateWithoutLookupLimit calls the callback with an iterator over the keys, but doesn't enforce default limit on opts. // IterateWithoutLookupLimit calls the callback with an iterator over the keys, but doesn't enforce default limit on opts.
IterateWithoutLookupLimit(ctx context.Context, opts IterateOptions, fn func(context.Context, Iterator) error) error IterateWithoutLookupLimit(ctx context.Context, opts IterateOptions, fn func(context.Context, Iterator) error) error
// CompareAndSwap atomically compares and swaps oldValue with newValue. // CompareAndSwap atomically compares and swaps oldValue with newValue.

View File

@ -193,6 +193,17 @@ func (client *Client) Iterate(ctx context.Context, opts storage.IterateOptions,
return client.IterateWithoutLookupLimit(ctx, opts, fn) return client.IterateWithoutLookupLimit(ctx, opts, fn)
} }
// IterateUnordered iterates over all data, however, does not guarantee ordering.
// It only guarantees all items are iterated at least once.
func (client *Client) IterateUnordered(ctx context.Context, fn func(context.Context, storage.Iterator) error) (err error) {
defer mon.Task()(&ctx)(&err)
return fn(ctx, &ScanIterator{
db: client.db,
it: client.db.Scan(ctx, 0, "*", 0).Iterator(),
})
}
// IterateWithoutLookupLimit calls the callback with an iterator over the keys, but doesn't enforce default limit on opts. // IterateWithoutLookupLimit calls the callback with an iterator over the keys, but doesn't enforce default limit on opts.
func (client *Client) IterateWithoutLookupLimit(ctx context.Context, opts storage.IterateOptions, fn func(context.Context, storage.Iterator) error) (err error) { func (client *Client) IterateWithoutLookupLimit(ctx context.Context, opts storage.IterateOptions, fn func(context.Context, storage.Iterator) error) (err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)

View File

@ -8,6 +8,8 @@ import (
"context" "context"
"sort" "sort"
"github.com/go-redis/redis/v8"
"storj.io/storj/storage" "storj.io/storj/storage"
) )
@ -75,3 +77,29 @@ func (it *StaticIterator) Next(ctx context.Context, item *storage.ListItem) bool
it.Index++ it.Index++
return true return true
} }
// ScanIterator iterates over scan command items.
type ScanIterator struct {
db *redis.Client
it *redis.ScanIterator
}
// Next returns the next item from the iterator.
func (it *ScanIterator) Next(ctx context.Context, item *storage.ListItem) bool {
ok := it.it.Next(ctx)
if !ok {
return false
}
key := it.it.Val()
value, err := it.db.Get(ctx, key).Bytes()
if err != nil {
return false
}
item.Key = storage.Key(key)
item.Value = storage.Value(value)
item.IsPrefix = false
return true
}

View File

@ -100,6 +100,26 @@ func (store *Logger) Iterate(ctx context.Context, opts storage.IterateOptions, f
}) })
} }
// IterateUnordered iterates over all data, however, does not guarantee ordering.
// It only guarantees all items are iterated at least once.
func (store *Logger) IterateUnordered(ctx context.Context, fn func(context.Context, storage.Iterator) error) (err error) {
defer mon.Task()(&ctx)(&err)
store.log.Debug("IterateUnordered")
return store.store.IterateUnordered(ctx, func(ctx context.Context, it storage.Iterator) error {
return fn(ctx, storage.IteratorFunc(func(ctx context.Context, item *storage.ListItem) bool {
ok := it.Next(ctx, item)
if ok {
store.log.Debug(" ",
zap.ByteString("key", item.Key),
zap.Int("value length", len(item.Value)),
zap.Binary("truncated value", truncate(item.Value)),
)
}
return ok
}))
})
}
// IterateWithoutLookupLimit calls the callback with an iterator over the keys, but doesn't enforce default limit on opts. // IterateWithoutLookupLimit calls the callback with an iterator over the keys, but doesn't enforce default limit on opts.
func (store *Logger) IterateWithoutLookupLimit(ctx context.Context, opts storage.IterateOptions, fn func(context.Context, storage.Iterator) error) (err error) { func (store *Logger) IterateWithoutLookupLimit(ctx context.Context, opts storage.IterateOptions, fn func(context.Context, storage.Iterator) error) (err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)

View File

@ -237,6 +237,13 @@ func (store *Client) Iterate(ctx context.Context, opts storage.IterateOptions, f
return store.IterateWithoutLookupLimit(ctx, opts, fn) return store.IterateWithoutLookupLimit(ctx, opts, fn)
} }
// IterateUnordered iterates over all data, however, does not guarantee ordering.
// It only guarantees all items are iterated at least once.
func (store *Client) IterateUnordered(ctx context.Context, fn func(context.Context, storage.Iterator) error) (err error) {
defer mon.Task()(&ctx)(&err)
return store.IterateWithoutLookupLimit(ctx, storage.IterateOptions{}, fn)
}
// IterateWithoutLookupLimit calls the callback with an iterator over the keys, but doesn't enforce default limit on opts. // IterateWithoutLookupLimit calls the callback with an iterator over the keys, but doesn't enforce default limit on opts.
func (store *Client) IterateWithoutLookupLimit(ctx context.Context, opts storage.IterateOptions, fn func(context.Context, storage.Iterator) error) (err error) { func (store *Client) IterateWithoutLookupLimit(ctx context.Context, opts storage.IterateOptions, fn func(context.Context, storage.Iterator) error) (err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)