Skip to content

Commit 1b729d7

Browse files
committed
Fix pagination for GetMultipleKeys
1 parent 7477ec1 commit 1b729d7

File tree

4 files changed

+44
-23
lines changed

4 files changed

+44
-23
lines changed

src/main/protobuf/fossildbapi.proto

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ message GetMultipleVersionsReply {
6161

6262
message GetMultipleKeysRequest {
6363
required string collection = 1;
64-
required string key = 2;
64+
optional string startAfterKey = 2;
6565
optional string prefix = 3;
6666
optional uint64 version = 4;
6767
optional uint32 limit = 5;

src/main/scala/com/scalableminds/fossildb/FossilDBGrpcImpl.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class FossilDBGrpcImpl(storeManager: StoreManager)
5151

5252
override def getMultipleKeys(req: GetMultipleKeysRequest): Future[GetMultipleKeysReply] = withExceptionHandler(req) {
5353
val store = storeManager.getStore(req.collection)
54-
val (keys, values, versions) = store.getMultipleKeys(req.key, req.prefix, req.version, req.limit)
54+
val (keys, values, versions) = store.getMultipleKeys(req.startAfterKey, req.prefix, req.version, req.limit)
5555
GetMultipleKeysReply(success = true, None, keys, values.map(ByteString.copyFrom), versions)
5656
} { errorMsg => GetMultipleKeysReply(success = false, errorMsg) }
5757

src/main/scala/com/scalableminds/fossildb/db/VersionedKeyValueStore.scala

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import scala.util.Try
55

66

77
case class VersionedKey(key: String, version: Long) {
8-
override def toString: String = s"$key@${(~version).toHexString.toUpperCase}@$version"
8+
override def toString: String = s"$key${VersionedKey.versionSeparator}${(~version).toHexString.toUpperCase}${VersionedKey.versionSeparator}$version"
99
}
1010

1111
object VersionedKey {
1212

13+
val versionSeparator: Char = '@'
14+
1315
def apply(key: String): Option[VersionedKey] = {
14-
val parts = key.split('@')
16+
val parts = key.split(versionSeparator)
1517
for {
1618
key <- parts.headOption
1719
versionString <- parts.lastOption
@@ -91,8 +93,6 @@ class VersionedKeyValueStore(underlying: RocksDBStore) {
9193
def get(key: String, version: Option[Long] = None): Option[VersionedKeyValuePair[Array[Byte]]] =
9294
scanVersionValuePairs(key, version).toStream.headOption
9395

94-
def getUnderlying: RocksDBStore = underlying
95-
9696
def getMultipleVersions(key: String, oldestVersion: Option[Long] = None, newestVersion: Option[Long] = None): (List[Array[Byte]], List[Long]) = {
9797

9898
@tailrec
@@ -113,33 +113,54 @@ class VersionedKeyValueStore(underlying: RocksDBStore) {
113113

114114
private def scanVersionValuePairs(key: String, version: Option[Long] = None): Iterator[VersionedKeyValuePair[Array[Byte]]] = {
115115
requireValidKey(key)
116-
val prefix = s"$key@"
116+
val prefix = s"$key${VersionedKey.versionSeparator}"
117117
underlying.scan(version.map(VersionedKey(key, _).toString).getOrElse(prefix), Some(prefix)).flatMap { pair =>
118118
VersionedKey(pair.key).map(VersionedKeyValuePair(_, pair.value))
119119
}
120120
}
121121

122122
private def scanVersionsOnly(key: String, version: Option[Long] = None): Iterator[VersionedKey] = {
123123
requireValidKey(key)
124-
val prefix = s"$key@"
124+
val prefix = s"$key${VersionedKey.versionSeparator}"
125125
underlying.scanKeysOnly(version.map(VersionedKey(key, _).toString).getOrElse(prefix), Some(prefix)).flatMap { key =>
126126
VersionedKey(key)
127127
}
128128
}
129129

130-
def getMultipleKeys(key: String, prefix: Option[String] = None, version: Option[Long] = None, limit: Option[Int]): (Seq[String], Seq[Array[Byte]], Seq[Long]) = {
131-
requireValidKey(key)
130+
def getMultipleKeys(startAfterKey: Option[String], prefix: Option[String] = None, version: Option[Long] = None, limit: Option[Int]): (Seq[String], Seq[Array[Byte]], Seq[Long]) = {
131+
startAfterKey.foreach(requireValidKey)
132132
prefix.foreach{ p => requireValidKey(p)}
133-
val iterator: VersionFilterIterator = scanKeys(key, prefix, version)
134-
val asSequence = iterator.take(limit.getOrElse(Int.MaxValue)).toSeq
135-
val keys = asSequence.map(_.key)
136-
val values = asSequence.map(_.value)
137-
val versions = asSequence.map(_.version)
133+
val iterator: VersionFilterIterator = scanKeys(startAfterKey, prefix, version)
134+
135+
/*
136+
Note that seek in the underlying iterators either hits precisely or goes to the
137+
lexicographically *next* key. To achieve correct behavior with startAfterKey,
138+
we have to advance once in case of the exact hit.
139+
*/
140+
val firstItemOpt: Option[VersionedKeyValuePair[Array[Byte]]] = if (iterator.hasNext) {
141+
val firstItem = iterator.next()
142+
if (startAfterKey.contains(firstItem.key)) {
143+
None
144+
} else {
145+
Some(firstItem)
146+
}
147+
} else None
148+
149+
val limitPadded = limit.map(_ - 1).getOrElse(Int.MaxValue)
150+
val asVector = iterator.take(limitPadded).toVector
151+
val asSequenceAdvancedIfNeeded = firstItemOpt.map(_ +: asVector).getOrElse(asVector).take(limit.getOrElse(Int.MaxValue))
152+
val keys = asSequenceAdvancedIfNeeded.map(_.key)
153+
println(s"Returning keys ${keys.toList}")
154+
val values = asSequenceAdvancedIfNeeded.map(_.value)
155+
val versions = asSequenceAdvancedIfNeeded.map(_.version)
138156
(keys, values, versions)
139157
}
140158

141-
private def scanKeys(key: String, prefix: Option[String] = None, version: Option[Long] = None): VersionFilterIterator =
142-
new VersionFilterIterator(underlying.scan(key, prefix), version)
159+
private def scanKeys(startAfterKey: Option[String], prefix: Option[String] = None, version: Option[Long] = None): VersionFilterIterator = {
160+
val fullKey = startAfterKey.map(key => s"$key${VersionedKey.versionSeparator}").getOrElse("")
161+
println(s"Scanning to $fullKey")
162+
new VersionFilterIterator(underlying.scan(fullKey, prefix), version)
163+
}
143164

144165
def deleteMultipleVersions(key: String, oldestVersion: Option[Long] = None, newestVersion: Option[Long] = None): Unit = {
145166
@tailrec
@@ -178,6 +199,6 @@ class VersionedKeyValueStore(underlying: RocksDBStore) {
178199
}
179200

180201
private def requireValidKey(key: String): Unit = {
181-
require(!(key contains "@"), "keys cannot contain the char @")
202+
require(!key.contains(VersionedKey.versionSeparator), s"keys cannot contain the char ${VersionedKey.versionSeparator}")
182203
}
183204
}

src/test/scala/com/scalableminds/fossildb/FossilDBSuite.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ class FossilDBSuite extends FlatSpec with BeforeAndAfterEach with TestHelpers wi
206206
client.put(PutRequest(collectionA, aKey, Some(0), testData1))
207207
client.put(PutRequest(collectionA, anotherKey, Some(0), testData2))
208208
client.put(PutRequest(collectionA, aThirdKey, Some(0), testData3))
209-
val reply = client.getMultipleKeys(GetMultipleKeysRequest(collectionA, aThirdKey))
209+
val reply = client.getMultipleKeys(GetMultipleKeysRequest(collectionA, Some(aKey)))
210210
assert(reply.keys.length == 2)
211211
assert(reply.keys.contains(anotherKey))
212212
assert(reply.keys.contains(aThirdKey))
@@ -227,8 +227,8 @@ class FossilDBSuite extends FlatSpec with BeforeAndAfterEach with TestHelpers wi
227227
client.put(PutRequest(collectionA, aKey, Some(2), testData3))
228228
client.put(PutRequest(collectionA, anotherKey, Some(2), testData3))
229229
client.put(PutRequest(collectionA, aThirdKey, Some(2), testData3))
230-
val reply = client.getMultipleKeys(GetMultipleKeysRequest(collectionA, aKey, None, Some(1)))
231-
assert(reply.keys.length == 3)
230+
val reply = client.getMultipleKeys(GetMultipleKeysRequest(collectionA, Some(aKey), None, Some(1)))
231+
assert(reply.keys.length == 2)
232232
assert(reply.values.contains(testData2))
233233
}
234234

@@ -242,7 +242,7 @@ class FossilDBSuite extends FlatSpec with BeforeAndAfterEach with TestHelpers wi
242242
client.put(PutRequest(collectionA, aKey, Some(2), testData3))
243243
client.put(PutRequest(collectionA, anotherKey, Some(2), testData3))
244244
client.put(PutRequest(collectionA, aThirdKey, Some(2), testData3))
245-
val reply = client.getMultipleKeys(GetMultipleKeysRequest(collectionA, aKey, Some("aK"), Some(1)))
245+
val reply = client.getMultipleKeys(GetMultipleKeysRequest(collectionA, None, Some("aK"), Some(1)))
246246
assert(reply.keys.length == 1)
247247
assert(reply.values.contains(testData2))
248248
assert(reply.actualVersions.contains(1))
@@ -258,7 +258,7 @@ class FossilDBSuite extends FlatSpec with BeforeAndAfterEach with TestHelpers wi
258258
client.put(PutRequest(collectionA, aKey, Some(2), testData3))
259259
client.put(PutRequest(collectionA, anotherKey, Some(2), testData3))
260260
client.put(PutRequest(collectionA, aThirdKey, Some(2), testData3))
261-
val reply = client.getMultipleKeys(GetMultipleKeysRequest(collectionA, aKey, None, Some(1), Some(2)))
261+
val reply = client.getMultipleKeys(GetMultipleKeysRequest(collectionA, None, None, Some(1), Some(2)))
262262
assert(reply.keys.length == 2)
263263
assert(reply.values.contains(testData2))
264264
assert(reply.actualVersions.contains(1))

0 commit comments

Comments
 (0)