Skip to content

Commit eac2883

Browse files
committed
Throw NoSuchKeyException when key doesn't exist in get
Motivation: Such checks are best performed in backend than having client checking empty or null value. This is also in-line with more critical requirement for functioning of NotFound introduced by MonitorUpdatingPersister Design. Even though there are some popular KVStore's which do not throw such exception on keyNotFound such as AWS-DDB (arguably the most popular one), we can still figure out that key didn't exist if value was returned as null or by similar means in other stores.
1 parent f428602 commit eac2883

File tree

6 files changed

+45
-28
lines changed

6 files changed

+45
-28
lines changed

app/src/main/java/org/vss/api/AbstractVssApi.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.vss.ErrorResponse;
99
import org.vss.KVStore;
1010
import org.vss.exception.ConflictException;
11+
import org.vss.exception.NoSuchKeyException;
1112

1213
public abstract class AbstractVssApi {
1314
final KVStore kvStore;
@@ -32,6 +33,8 @@ Response toErrorResponse(Exception e) {
3233
} else if (e instanceof IllegalArgumentException
3334
|| e instanceof InvalidProtocolBufferException) {
3435
errorCode = ErrorCode.INVALID_REQUEST_EXCEPTION;
36+
} else if (e instanceof NoSuchKeyException) {
37+
errorCode = ErrorCode.NO_SUCH_KEY_EXCEPTION;
3538
} else {
3639
errorCode = ErrorCode.INTERNAL_SERVER_EXCEPTION;
3740
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.vss.exception;
2+
3+
public class NoSuchKeyException extends RuntimeException {
4+
public NoSuchKeyException(String message) {
5+
super(message);
6+
}
7+
}

app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.vss.PutObjectRequest;
2424
import org.vss.PutObjectResponse;
2525
import org.vss.exception.ConflictException;
26+
import org.vss.exception.NoSuchKeyException;
2627
import org.vss.postgres.tables.records.VssDbRecord;
2728

2829
import static org.vss.postgres.tables.VssDb.VSS_DB;
@@ -31,6 +32,11 @@
3132
public class PostgresBackendImpl implements KVStore {
3233

3334
private static final int LIST_KEY_VERSIONS_MAX_PAGE_SIZE = 100;
35+
private static final KeyValue DEFAULT_GLOBAL_VERSION_KV = KeyValue.newBuilder()
36+
.setKey(GLOBAL_VERSION_KEY)
37+
.setValue(ByteString.EMPTY)
38+
.setVersion(0L)
39+
.build();
3440
private final DSLContext context;
3541

3642
@Inject
@@ -47,16 +53,19 @@ public GetObjectResponse get(GetObjectRequest request) {
4753
.fetchOne();
4854

4955
final KeyValue keyValue;
50-
51-
if (vssDbRecord != null) {
56+
if (vssDbRecord == null) {
57+
if (GLOBAL_VERSION_KEY.equals(request.getKey())) {
58+
keyValue = DEFAULT_GLOBAL_VERSION_KV;
59+
} else {
60+
throw new NoSuchKeyException(
61+
"Specified key: " + request.getKey() + " in request does not exist.");
62+
}
63+
} else {
5264
keyValue = KeyValue.newBuilder()
5365
.setKey(vssDbRecord.getKey())
5466
.setValue(ByteString.copyFrom(vssDbRecord.getValue()))
5567
.setVersion(vssDbRecord.getVersion())
5668
.build();
57-
} else {
58-
keyValue = KeyValue.newBuilder()
59-
.setKey(request.getKey()).build();
6069
}
6170

6271
return GetObjectResponse.newBuilder()

app/src/main/proto/vss.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ message GetObjectRequest {
1616

1717
// `Key` for which the value is to be fetched.
1818
//
19+
// If the specified `key` does not exist, returns `ErrorResponse` with `NO_SUCH_KEY_EXCEPTION` as
20+
// `ErrorCode`.
21+
//
1922
// Consistency Guarantee:
2023
// Get(read) operations against a `key` are consistent reads and will reflect all previous writes,
2124
// since Put/Write provides read-after-write and read-after-update consistency guarantees.
@@ -276,6 +279,9 @@ enum ErrorCode {
276279
// An internal server error occurred, client is probably at no fault and can safely retry this
277280
// error with exponential backoff.
278281
INTERNAL_SERVER_EXCEPTION = 3;
282+
283+
// NO_SUCH_KEY_EXCEPTION is used when the specified `key` in a `GetObjectRequest` does not exist.
284+
NO_SUCH_KEY_EXCEPTION = 4;
279285
}
280286

281287
// Represents a key-value pair to be stored or retrieved.

app/src/test/java/org/vss/AbstractKVStoreIntegrationTest.java

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.junit.jupiter.api.Test;
1212
import org.testcontainers.shaded.org.apache.commons.lang3.StringUtils;
1313
import org.vss.exception.ConflictException;
14+
import org.vss.exception.NoSuchKeyException;
1415

1516
import static org.hamcrest.MatcherAssert.assertThat;
1617
import static org.hamcrest.Matchers.is;
@@ -19,7 +20,6 @@
1920
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
2021
import static org.junit.jupiter.api.Assertions.assertFalse;
2122
import static org.junit.jupiter.api.Assertions.assertThrows;
22-
import static org.junit.jupiter.api.Assertions.assertTrue;
2323

2424
public abstract class AbstractKVStoreIntegrationTest {
2525

@@ -153,25 +153,25 @@ void putAndDeleteShouldSucceedAsAtomicTransaction() {
153153
assertThat(response.getVersion(), is(1L));
154154
assertThat(response.getValue().toStringUtf8(), is("k2v1"));
155155

156-
assertTrue(getObject("k1").getValue().isEmpty());
156+
assertThrows(NoSuchKeyException.class, () -> getObject("k1"));
157157

158158
// Delete fails (and hence put as well) due to mismatched version for the deleted item.
159159
assertThrows(ConflictException.class, () -> putAndDeleteObjects(null, List.of(kv("k3", "k3v1", 0)), List.of(kv("k2", "", 3))));
160160

161-
assertTrue(getObject("k3").getValue().isEmpty());
162-
assertFalse(getObject("k2").getValue().isEmpty());
161+
assertThrows(NoSuchKeyException.class, () -> getObject("k3"));
162+
assertDoesNotThrow(() -> getObject("k2"));
163163

164164
// Put fails (and hence delete as well) due to mismatched version for the put item.
165165
assertThrows(ConflictException.class, () -> putAndDeleteObjects(null, List.of(kv("k3", "k3v1", 1)), List.of(kv("k2", "", 1))));
166166

167-
assertTrue(getObject("k3").getValue().isEmpty());
168-
assertFalse(getObject("k2").getValue().isEmpty());
167+
assertThrows(NoSuchKeyException.class, () -> getObject("k3"));
168+
assertDoesNotThrow(() -> getObject("k2"));
169169

170170
// Put and delete both fail due to mismatched global version.
171171
assertThrows(ConflictException.class, () -> putAndDeleteObjects(2L, List.of(kv("k3", "k3v1", 0)), List.of(kv("k2", "", 1))));
172172

173-
assertTrue(getObject("k3").getValue().isEmpty());
174-
assertFalse(getObject("k2").getValue().isEmpty());
173+
assertThrows(NoSuchKeyException.class, () -> getObject("k3"));
174+
assertDoesNotThrow(() -> getObject("k2"));
175175

176176
assertThat(getObject(KVStore.GLOBAL_VERSION_KEY).getVersion(), is(0L));
177177
}
@@ -182,18 +182,14 @@ void deleteShouldSucceedWhenItemExists() {
182182
// Conditional Delete
183183
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
184184

185-
KeyValue response = getObject("k1");
186-
assertThat(response.getKey(), is("k1"));
187-
assertTrue(response.getValue().isEmpty());
185+
assertThrows(NoSuchKeyException.class, () -> getObject("k1"));
188186

189187
assertDoesNotThrow(() -> putObjects(null, List.of(kv("k1", "k1v1", 0))));
190188
assertDoesNotThrow(() -> putObjects(null, List.of(kv("k1", "k1v2", 1))));
191189
// NonConditional Delete
192190
assertDoesNotThrow(() -> deleteObject(kv("k1", "", -1)));
193191

194-
response = getObject("k1");
195-
assertThat(response.getKey(), is("k1"));
196-
assertTrue(response.getValue().isEmpty());
192+
assertThrows(NoSuchKeyException.class, () -> getObject("k1"));
197193
}
198194

199195
@Test
@@ -207,18 +203,12 @@ void deleteShouldBeIdempotent() {
207203
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
208204
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
209205

210-
KeyValue response = getObject("k1");
211-
assertThat(response.getKey(), is("k1"));
212-
assertTrue(response.getValue().isEmpty());
206+
assertThrows(NoSuchKeyException.class, () -> getObject("k1"));
213207
}
214208

215209
@Test
216-
void getShouldReturnEmptyResponseWhenKeyDoesNotExist() {
217-
KeyValue response = getObject("non_existent_key");
218-
219-
assertThat(response.getKey(), is("non_existent_key"));
220-
assertTrue(response.getValue().isEmpty());
221-
assertThat(response.getVersion(), is(0L));
210+
void getShouldThrowNoSuchKeyExceptionWhenKeyDoesNotExist() {
211+
assertThrows(NoSuchKeyException.class, () -> getObject("non_existent_key"));
222212
}
223213

224214
@Test

app/src/test/java/org/vss/api/GetObjectApiTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.vss.KVStore;
1717
import org.vss.KeyValue;
1818
import org.vss.exception.ConflictException;
19+
import org.vss.exception.NoSuchKeyException;
1920

2021
import static org.hamcrest.MatcherAssert.assertThat;
2122
import static org.hamcrest.Matchers.is;
@@ -80,6 +81,7 @@ private static Stream<Arguments> provideErrorTestCases() {
8081
return Stream.of(
8182
Arguments.of(new ConflictException(""), ErrorCode.CONFLICT_EXCEPTION),
8283
Arguments.of(new IllegalArgumentException(""), ErrorCode.INVALID_REQUEST_EXCEPTION),
84+
Arguments.of(new NoSuchKeyException(""), ErrorCode.NO_SUCH_KEY_EXCEPTION),
8385
Arguments.of(new RuntimeException(""), ErrorCode.INTERNAL_SERVER_EXCEPTION)
8486
);
8587
}

0 commit comments

Comments
 (0)