diff --git a/storage/redis/redis.go b/storage/redis/redis.go index fc0c9898..48154295 100644 --- a/storage/redis/redis.go +++ b/storage/redis/redis.go @@ -133,27 +133,41 @@ func (s *redisStorage) Close() error { return nil } +// redisIterator is populated with the results of an HSCAN call for the table's key (https://redis.io/commands/scan/). +// This result is a single-dimension array that contains [n] == key, [n+1] == value. type redisIterator struct { - current uint64 - keys []string - client *redis.Client - hash string + current uint64 + keys []string + client *redis.Client + hash string + initialized bool } func (i *redisIterator) exhausted() bool { - return uint64(len(i.keys)) <= i.current + return uint64(len(i.keys)) <= i.current+1 } -func (i *redisIterator) Next() bool { - i.current++ - if string(i.Key()) == offsetKey { - i.current++ +func (i *redisIterator) ignoreOffsetKey() bool { + if i.exhausted() { + return false + } + if string(i.keys[i.current]) == offsetKey { + i.current = i.current + 2 } return !i.exhausted() } +func (i *redisIterator) Next() bool { + if !i.initialized { + i.initialized = true + } else { + i.current = i.current + 2 + } + return i.ignoreOffsetKey() +} + func (i *redisIterator) Key() []byte { - if i.exhausted() { + if !i.ignoreOffsetKey() { return nil } key := i.keys[i.current] @@ -165,11 +179,10 @@ func (i *redisIterator) Err() error { } func (i *redisIterator) Value() ([]byte, error) { - if i.exhausted() { + if !i.ignoreOffsetKey() { return nil, nil } - key := i.keys[i.current] - return i.client.HGet(i.hash, key).Bytes() + return []byte(i.keys[i.current+1]), nil } func (i *redisIterator) Release() { diff --git a/storage/redis/redis_test.go b/storage/redis/redis_test.go new file mode 100644 index 00000000..3604cebd --- /dev/null +++ b/storage/redis/redis_test.go @@ -0,0 +1,72 @@ +package redis + +import ( + "runtime/debug" + "testing" +) + +func Test_redisIterator(t *testing.T) { + assertRedisIterator(t, &redisIterator{ + current: 0, + keys: []string{ + "key1", + "val1", + offsetKey, + "123", + "key2", + "val2", + }, + }) + assertRedisIterator(t, &redisIterator{ + current: 0, + keys: []string{ + offsetKey, + "123", + "key1", + "val1", + "key2", + "val2", + }, + }) + assertRedisIterator(t, &redisIterator{ + current: 0, + keys: []string{ + "key1", + "val1", + "key2", + "val2", + offsetKey, + "123", + }, + }) +} + +func assertRedisIterator(t *testing.T, it *redisIterator) { + // the iterator contract implies we must call `Next()` first + it.Next() + assertRedisIteratorKey(t, it, "key1", "val1") + + it.Next() + assertRedisIteratorKey(t, it, "key2", "val2") + + it.Next() + if !it.exhausted() { + t.Fatalf("Expected iterator to be exhausted in %s", string(debug.Stack())) + } +} + +func assertRedisIteratorKey(t *testing.T, it *redisIterator, expectedKey string, expectedValue string) { + if it.exhausted() { + t.Fatalf("Did not expect iterator to be exhausted in %s", string(debug.Stack())) + } + + actualKey := string(it.Key()) + if actualKey != expectedKey { + t.Fatalf("Expected iterator key to be '%s', but was '%s' in %s", expectedKey, actualKey, string(debug.Stack())) + } + + actualValue, _ := it.Value() + if string(actualValue) != expectedValue { + t.Fatalf("Expected iterator value to be '%s', but was '%s' in %s", expectedValue, actualValue, string(debug.Stack())) + } +}