Skip to content

Commit 9c94974

Browse files
authored
Merge pull request #15 from G8XSU/delete-api
Add Delete functionality
2 parents 909f7cc + 899347c commit 9c94974

File tree

7 files changed

+292
-7
lines changed

7 files changed

+292
-7
lines changed

app/src/main/java/org/vss/KVStore.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ public interface KVStore {
88

99
PutObjectResponse put(PutObjectRequest request);
1010

11+
DeleteObjectResponse delete(DeleteObjectRequest request);
12+
1113
ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request);
1214
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.vss.api;
2+
3+
import jakarta.inject.Inject;
4+
import jakarta.ws.rs.POST;
5+
import jakarta.ws.rs.Path;
6+
import jakarta.ws.rs.Produces;
7+
import jakarta.ws.rs.core.MediaType;
8+
import jakarta.ws.rs.core.Response;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.vss.DeleteObjectRequest;
11+
import org.vss.DeleteObjectResponse;
12+
import org.vss.KVStore;
13+
14+
@Path(VssApiEndpoint.DELETE_OBJECT)
15+
@Slf4j
16+
public class DeleteObjectApi extends AbstractVssApi {
17+
@Inject
18+
public DeleteObjectApi(KVStore kvstore) {
19+
super(kvstore);
20+
}
21+
22+
@POST
23+
@Produces(MediaType.APPLICATION_OCTET_STREAM)
24+
public Response execute(byte[] payload) {
25+
try {
26+
DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload);
27+
DeleteObjectResponse response = kvStore.delete(request);
28+
return toResponse(response);
29+
} catch (Exception e) {
30+
log.error("Exception in DeleteObjectApi: ", e);
31+
return toErrorResponse(e);
32+
}
33+
}
34+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
public class VssApiEndpoint {
44
public static final String GET_OBJECT = "/getObject";
55
public static final String PUT_OBJECTS = "/putObjects";
6+
public static final String DELETE_OBJECT = "/deleteObject";
67
public static final String LIST_KEY_VERSIONS = "/listKeyVersions";
78
}

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

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import org.jooq.Insert;
1212
import org.jooq.Query;
1313
import org.jooq.Update;
14+
import org.vss.DeleteObjectRequest;
15+
import org.vss.DeleteObjectResponse;
1416
import org.vss.GetObjectRequest;
1517
import org.vss.GetObjectResponse;
1618
import org.vss.KVStore;
@@ -66,23 +68,31 @@ public PutObjectResponse put(PutObjectRequest request) {
6668

6769
String storeId = request.getStoreId();
6870

69-
List<VssDbRecord> vssRecords = new ArrayList<>(request.getTransactionItemsList().stream()
71+
List<VssDbRecord> vssPutRecords = new ArrayList<>(request.getTransactionItemsList().stream()
72+
.map(kv -> buildVssRecord(storeId, kv)).toList());
73+
74+
List<VssDbRecord> vssDeleteRecords = new ArrayList<>(request.getDeleteItemsList().stream()
7075
.map(kv -> buildVssRecord(storeId, kv)).toList());
7176

7277
if (request.hasGlobalVersion()) {
7378
VssDbRecord globalVersionRecord = buildVssRecord(storeId,
7479
KeyValue.newBuilder()
7580
.setKey(GLOBAL_VERSION_KEY)
7681
.setVersion(request.getGlobalVersion())
82+
.setValue(ByteString.EMPTY)
7783
.build());
7884

79-
vssRecords.add(globalVersionRecord);
85+
vssPutRecords.add(globalVersionRecord);
8086
}
8187

8288
context.transaction((ctx) -> {
8389
DSLContext dsl = ctx.dsl();
84-
List<Query> batchQueries = vssRecords.stream()
85-
.map(vssRecord -> buildPutObjectQuery(dsl, vssRecord)).toList();
90+
List<Query> batchQueries = new ArrayList<>();
91+
92+
batchQueries.addAll(vssPutRecords.stream()
93+
.map(vssRecord -> buildPutObjectQuery(dsl, vssRecord)).toList());
94+
batchQueries.addAll(vssDeleteRecords.stream()
95+
.map(vssRecord -> buildDeleteObjectQuery(dsl, vssRecord)).toList());
8696

8797
int[] batchResult = dsl.batch(batchQueries).execute();
8898

@@ -97,6 +107,12 @@ public PutObjectResponse put(PutObjectRequest request) {
97107
return PutObjectResponse.newBuilder().build();
98108
}
99109

110+
private Query buildDeleteObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {
111+
return dsl.deleteFrom(VSS_DB).where(VSS_DB.STORE_ID.eq(vssRecord.getStoreId())
112+
.and(VSS_DB.KEY.eq(vssRecord.getKey()))
113+
.and(VSS_DB.VERSION.eq(vssRecord.getVersion())));
114+
}
115+
100116
private Query buildPutObjectQuery(DSLContext dsl, VssDbRecord vssRecord) {
101117
return vssRecord.getVersion() == 0 ? buildInsertRecordQuery(dsl, vssRecord)
102118
: buildUpdateRecordQuery(dsl, vssRecord);
@@ -126,6 +142,20 @@ private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
126142
.setVersion(kv.getVersion());
127143
}
128144

145+
@Override
146+
public DeleteObjectResponse delete(DeleteObjectRequest request) {
147+
String storeId = request.getStoreId();
148+
VssDbRecord vssDbRecord = buildVssRecord(storeId, request.getKeyValue());
149+
150+
context.transaction((ctx) -> {
151+
DSLContext dsl = ctx.dsl();
152+
Query deleteObjectQuery = buildDeleteObjectQuery(dsl, vssDbRecord);
153+
dsl.execute(deleteObjectQuery);
154+
});
155+
156+
return DeleteObjectResponse.newBuilder().build();
157+
}
158+
129159
@Override
130160
public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
131161
String storeId = request.getStoreId();

app/src/main/proto/vss.proto

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
syntax = "proto3";
2+
package vss;
23
option java_multiple_files = true;
3-
package org.vss;
4+
option java_package = "org.vss";
45

6+
// Request payload to be used for `GetObject` API call to server.
57
message GetObjectRequest {
68

79
// store_id is a keyspace identifier.
@@ -24,12 +26,14 @@ message GetObjectRequest {
2426
string key = 2;
2527
}
2628

29+
// Server response for `GetObject` API.
2730
message GetObjectResponse {
2831

2932
// Fetched value and version along with the corresponding key in the request.
3033
KeyValue value = 2;
3134
}
3235

36+
// Request payload to be used for `PutObject` API call to server.
3337
message PutObjectRequest {
3438

3539
// store_id is a keyspace identifier.
@@ -65,9 +69,9 @@ message PutObjectRequest {
6569
// Clients can choose to encrypt the keys client-side in order to obfuscate their usage patterns.
6670
// If the write is successful, the previous value corresponding to the key will be overwritten.
6771
//
68-
// Multiple items in transaction_items of a single PutObjectRequest are written in
72+
// Multiple items in transaction_items and delete_items of a single PutObjectRequest are written in
6973
// a database-transaction in an all-or-nothing fashion.
70-
// Items in a single PutObjectRequest must have distinct keys.
74+
// All Items in a single PutObjectRequest must have distinct keys.
7175
//
7276
// Clients are expected to store a version against every key.
7377
// The write will succeed if the current DB version against the key is the same as in the request.
@@ -93,11 +97,56 @@ message PutObjectRequest {
9397
// All PutObjectRequests are strongly consistent i.e. they provide read-after-write and
9498
// read-after-update consistency guarantees.
9599
repeated KeyValue transaction_items = 3;
100+
101+
// Items to be deleted as a result of this PutObjectRequest.
102+
//
103+
// Each item in the `delete_items` field consists of a key and its corresponding version.
104+
// The version is used to perform a version check before deleting the item.
105+
// The delete will only succeed if the current database version against the key is the same as the version
106+
// specified in the request.
107+
//
108+
// Fails with `CONFLICT_EXCEPTION` as the ErrorCode if:
109+
// * The requested item does not exist.
110+
// * The requested item does exist but there is a version-number mismatch with the one in the database.
111+
//
112+
// Multiple items in the `delete_items` field, along with the `transaction_items`, are written in a
113+
// database transaction in an all-or-nothing fashion.
114+
//
115+
// All items within a single `PutObjectRequest` must have distinct keys.
116+
repeated KeyValue delete_items = 4;
96117
}
97118

119+
// Server response for `PutObject` API.
98120
message PutObjectResponse {
99121
}
100122

123+
// Request payload to be used for `DeleteObject` API call to server.
124+
message DeleteObjectRequest {
125+
// store_id is a keyspace identifier.
126+
// Ref: https://en.wikipedia.org/wiki/Keyspace_(distributed_data_store)
127+
// All APIs operate within a single store_id.
128+
// It is up to clients to use single or multiple stores for their use-case.
129+
// This can be used for client-isolation/ rate-limiting / throttling on the server-side.
130+
// Authorization and billing can also be performed at the store_id level.
131+
string store_id = 1;
132+
133+
// Item to be deleted as a result of this DeleteObjectRequest.
134+
//
135+
// An item consists of a key and its corresponding version.
136+
// The item is only deleted if the current database version against the key is the same as the version
137+
// specified in the request.
138+
// This operation is idempotent, that is, multiple delete calls for the same item will not fail.
139+
//
140+
// If the requested item does not exist, this operation will not fail.
141+
// If you wish to perform stricter checks while deleting an item, consider using PutObject API.
142+
KeyValue key_value = 2;
143+
}
144+
145+
// Server response for `DeleteObject` API.
146+
message DeleteObjectResponse{
147+
}
148+
149+
// Request payload to be used for `ListKeyVersions` API call to server.
101150
message ListKeyVersionsRequest {
102151

103152
// store_id is a keyspace identifier.
@@ -133,6 +182,7 @@ message ListKeyVersionsRequest {
133182
optional string page_token = 4;
134183
}
135184

185+
// Server response for `ListKeyVersions` API.
136186
message ListKeyVersionsResponse {
137187

138188
// Fetched keys and versions.
@@ -206,6 +256,7 @@ enum ErrorCode {
206256
INTERNAL_SERVER_EXCEPTION = 3;
207257
}
208258

259+
// Represents KeyValue pair to be stored or retrieved.
209260
message KeyValue {
210261

211262
// Key against which the value is stored.

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,66 @@ void putShouldSucceedWhenNoGlobalVersionIsGiven() {
131131
assertThat(getObject(KVStore.GLOBAL_VERSION_KEY).getVersion(), is(0L));
132132
}
133133

134+
@Test
135+
void putAndDeleteShouldSucceedAsAtomicTransaction() {
136+
assertDoesNotThrow(() -> putObjects(null, List.of(kv("k1", "k1v1", 0))));
137+
// Put and Delete succeeds
138+
assertDoesNotThrow(() -> putAndDeleteObjects(null, List.of(kv("k2", "k2v1", 0)), List.of(kv("k1", "", 1))));
139+
140+
KeyValue response = getObject("k2");
141+
assertThat(response.getKey(), is("k2"));
142+
assertThat(response.getVersion(), is(1L));
143+
assertThat(response.getValue().toStringUtf8(), is("k2v1"));
144+
145+
assertTrue(getObject("k1").getValue().isEmpty());
146+
147+
// Delete fails (and hence put as well) due to mismatched version for the deleted item.
148+
assertThrows(ConflictException.class, () -> putAndDeleteObjects(null, List.of(kv("k3", "k3v1", 0)), List.of(kv("k2", "", 3))));
149+
150+
assertTrue(getObject("k3").getValue().isEmpty());
151+
assertFalse(getObject("k2").getValue().isEmpty());
152+
153+
// Put fails (and hence delete as well) due to mismatched version for the put item.
154+
assertThrows(ConflictException.class, () -> putAndDeleteObjects(null, List.of(kv("k3", "k3v1", 1)), List.of(kv("k2", "", 1))));
155+
156+
assertTrue(getObject("k3").getValue().isEmpty());
157+
assertFalse(getObject("k2").getValue().isEmpty());
158+
159+
// Put and delete both fail due to mismatched global version.
160+
assertThrows(ConflictException.class, () -> putAndDeleteObjects(2L, List.of(kv("k3", "k3v1", 0)), List.of(kv("k2", "", 1))));
161+
162+
assertTrue(getObject("k3").getValue().isEmpty());
163+
assertFalse(getObject("k2").getValue().isEmpty());
164+
165+
assertThat(getObject(KVStore.GLOBAL_VERSION_KEY).getVersion(), is(0L));
166+
}
167+
168+
@Test
169+
void deleteShouldSucceedWhenItemExists() {
170+
assertDoesNotThrow(() -> putObjects(null, List.of(kv("k1", "k1v1", 0))));
171+
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
172+
173+
KeyValue response = getObject("k1");
174+
assertThat(response.getKey(), is("k1"));
175+
assertTrue(response.getValue().isEmpty());
176+
}
177+
178+
@Test
179+
void deleteShouldSucceedWhenItemDoesNotExist() {
180+
assertDoesNotThrow(() -> deleteObject(kv("non_existent_key", "", 0)));
181+
}
182+
183+
@Test
184+
void deleteShouldBeIdempotent() {
185+
assertDoesNotThrow(() -> putObjects(null, List.of(kv("k1", "k1v1", 0))));
186+
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
187+
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
188+
189+
KeyValue response = getObject("k1");
190+
assertThat(response.getKey(), is("k1"));
191+
assertTrue(response.getValue().isEmpty());
192+
}
193+
134194
@Test
135195
void getShouldReturnEmptyResponseWhenKeyDoesNotExist() {
136196
KeyValue response = getObject("non_existent_key");
@@ -370,6 +430,25 @@ private void putObjects(@Nullable Long globalVersion, List<KeyValue> keyValues)
370430
this.kvStore.put(putObjectRequestBuilder.build());
371431
}
372432

433+
private void putAndDeleteObjects(@Nullable Long globalVersion, List<KeyValue> putKeyValues, List<KeyValue> deleteKeyValues) {
434+
PutObjectRequest.Builder putObjectRequestBuilder = PutObjectRequest.newBuilder()
435+
.setStoreId(STORE_ID)
436+
.addAllTransactionItems(putKeyValues)
437+
.addAllDeleteItems(deleteKeyValues);
438+
439+
if (Objects.nonNull(globalVersion)) {
440+
putObjectRequestBuilder.setGlobalVersion(globalVersion);
441+
}
442+
443+
this.kvStore.put(putObjectRequestBuilder.build());
444+
}
445+
446+
private void deleteObject(KeyValue keyValue) {
447+
DeleteObjectRequest request = DeleteObjectRequest.newBuilder()
448+
.setStoreId(STORE_ID).setKeyValue(keyValue).build();
449+
this.kvStore.delete(request);
450+
}
451+
373452
private ListKeyVersionsResponse list(@Nullable String nextPageToken, @Nullable Integer pageSize,
374453
@Nullable String keyPrefix) {
375454
ListKeyVersionsRequest.Builder listRequestBuilder = ListKeyVersionsRequest.newBuilder()

0 commit comments

Comments
 (0)