From 29ce12f8f9f0284c85d7a1fe5f4b1c16c3ca9922 Mon Sep 17 00:00:00 2001 From: Tushar Naik Date: Fri, 8 Sep 2023 23:59:46 +0530 Subject: [PATCH] Added optional support for refIds --- .../aerospike/AerospikeClientHelpers.java | 116 ++++++++++++++++ .../aerospike/AerospikeConfiguration.java | 19 +++ .../crudstore/aerospike/AerospikeStore.java | 126 +++++++++++++++--- .../aerospike/AerospikeStoreSetting.java | 11 ++ .../crudstore/aerospike/ErrorHandler.java | 6 + .../aerospike/AerospikeStoreTest.java | 76 +++++++++++ .../data/UserDataWithReferences.java | 32 +++++ .../stores/UserAerospikeReplaceStore.java | 4 +- .../crudstore/aerospike/util/DataUtils.java | 14 ++ .../crudstore/aerospike/util/TestUtils.java | 25 ++++ .../com/livetheoogway/crudstore/core/Id.java | 7 + .../livetheoogway/crudstore/core/Store.java | 4 + .../crudstore/core/CachingStoreTest.java | 11 +- pom.xml | 3 +- 14 files changed, 430 insertions(+), 24 deletions(-) create mode 100644 aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeClientHelpers.java create mode 100644 aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeConfiguration.java create mode 100644 aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeStoreSetting.java create mode 100644 aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/data/UserDataWithReferences.java diff --git a/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeClientHelpers.java b/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeClientHelpers.java new file mode 100644 index 0000000..7dc75a1 --- /dev/null +++ b/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeClientHelpers.java @@ -0,0 +1,116 @@ +package com.livetheoogway.crudstore.aerospike; + +import com.aerospike.client.AerospikeClient; +import com.aerospike.client.Host; +import com.aerospike.client.IAerospikeClient; +import com.aerospike.client.Language; +import com.aerospike.client.policy.ClientPolicy; +import com.aerospike.client.policy.CommitLevel; +import com.aerospike.client.policy.Policy; +import com.aerospike.client.policy.ReadModeAP; +import com.aerospike.client.policy.Replica; +import com.aerospike.client.policy.ScanPolicy; +import com.aerospike.client.policy.TlsPolicy; +import com.aerospike.client.policy.WritePolicy; +import com.aerospike.client.task.RegisterTask; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.concurrent.Executors; + +@Slf4j +@UtilityClass +public class AerospikeClientHelpers { + + public IAerospikeClient aerospikeClient(AerospikeConfiguration config) { + + final var connectionString = config.hosts().trim(); + final var hosts = connectionString.split(","); + + final var readPolicy = new Policy(); + readPolicy.maxRetries = config.retries(); + readPolicy.replica = Replica.MASTER_PROLES; + readPolicy.sleepBetweenRetries = config.sleepBetweenRetries(); + readPolicy.totalTimeout = config.timeout(); + readPolicy.sendKey = true; + + final var writePolicy = new WritePolicy(); + writePolicy.maxRetries = config.retries(); + writePolicy.replica = Replica.MASTER_PROLES; + writePolicy.sleepBetweenRetries = config.sleepBetweenRetries(); + writePolicy.commitLevel = CommitLevel.COMMIT_ALL; + writePolicy.totalTimeout = config.timeout(); + writePolicy.sendKey = true; + writePolicy.expiration = -1; + + final var scanPolicy = new ScanPolicy(); + scanPolicy.maxRetries = 0; + scanPolicy.includeBinData = true; + scanPolicy.concurrentNodes = true; + scanPolicy.maxConcurrentNodes = hosts.length; + + final var clientPolicy = new ClientPolicy(); + clientPolicy.maxConnsPerNode = config.maxConnectionsPerNode(); + clientPolicy.readPolicyDefault = readPolicy; + clientPolicy.writePolicyDefault = writePolicy; + clientPolicy.scanPolicyDefault = scanPolicy; + clientPolicy.failIfNotConnected = true; + clientPolicy.threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 4); + + final var authEnabled = stringIsNotNullOrEmpty(config.user()) && stringIsNotNullOrEmpty(config.password()); + var defaultPort = authEnabled ? 4333 : 3000; + if (config.port() > 10) { + defaultPort = config.port(); + } + + var finalDefaultPort = defaultPort; + final Host[] aerospikeHosts = Arrays + .stream(hosts) + .map(host -> { + String[] hostItems = host.trim().split(":"); + if (hostItems.length == 2) { + return getHost(hostItems[0], Integer.parseInt(hostItems[1]), config); + } else { + return getHost(hostItems[0], finalDefaultPort, config); + } + }) + .toArray(Host[]::new); + + if (authEnabled) { + clientPolicy.user = config.user(); + clientPolicy.password = config.password(); + clientPolicy.tlsPolicy = new TlsPolicy(); + } + return new AerospikeClient(clientPolicy, aerospikeHosts); + } + + public void registerUDFs(final IAerospikeClient aerospikeClient, + final AerospikeConfiguration aerospikeConfiguration, + final String luaFilePath, + final String serverPath) { + Policy policy = new Policy(); + policy.maxRetries = aerospikeConfiguration.retries(); + policy.readModeAP = ReadModeAP.ALL; + policy.sleepBetweenRetries = aerospikeConfiguration.sleepBetweenRetries(); + policy.setTimeout(aerospikeConfiguration.timeout()); + policy.sendKey = true; + policy.replica = Replica.MASTER_PROLES; + log.info("Registering UDF modules now.."); + RegisterTask task = aerospikeClient.register(policy, luaFilePath, serverPath, Language.LUA); + task.waitTillComplete(); + log.info("Register client path {} and server path {}", luaFilePath, serverPath); + } + + private static Host getHost(String hostname, int port, AerospikeConfiguration config) { + if (stringIsNotNullOrEmpty(config.tlsName())) { + return new Host(hostname, config.tlsName(), port); + } else { + return new Host(hostname, port); + } + } + + private static boolean stringIsNotNullOrEmpty(String string) { + return string != null && !string.isEmpty(); + } +} diff --git a/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeConfiguration.java b/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeConfiguration.java new file mode 100644 index 0000000..cd2a209 --- /dev/null +++ b/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeConfiguration.java @@ -0,0 +1,19 @@ +package com.livetheoogway.crudstore.aerospike; + +import lombok.Builder; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +@Builder +public record AerospikeConfiguration( + @NotNull String hosts, + @Max(65535) int port, + @NotNull @Min(0) Integer retries, + @NotNull @Min(0) Integer sleepBetweenRetries, + @NotNull @Min(0) Integer timeout, + @NotNull @Min(1) Integer maxConnectionsPerNode, + String user, + String password, + String tlsName) {} diff --git a/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeStore.java b/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeStore.java index 298d248..66838da 100644 --- a/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeStore.java +++ b/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeStore.java @@ -21,19 +21,26 @@ import com.aerospike.client.Record; import com.aerospike.client.policy.RecordExistsAction; import com.aerospike.client.policy.WritePolicy; +import com.aerospike.client.query.Filter; +import com.aerospike.client.query.IndexCollectionType; +import com.aerospike.client.query.IndexType; +import com.aerospike.client.query.RecordSet; +import com.aerospike.client.query.Statement; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.livetheoogway.crudstore.core.Id; import com.livetheoogway.crudstore.core.Store; import lombok.extern.slf4j.Slf4j; -import lombok.val; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -42,7 +49,10 @@ @Slf4j public abstract class AerospikeStore implements Store { - protected static final String DATA = "data"; + private static final String DEFAULT_DATA_BIN = "data"; + private static final String DEFAULT_REF_ID_BIN = "refId"; + private static final String DEFAULT_REF_ID_INDEX_SUFFIX = "_idx"; + protected final ObjectMapper mapper; protected final IAerospikeClient client; protected final NamespaceSet namespaceSet; @@ -50,13 +60,14 @@ public abstract class AerospikeStore implements Store { protected final WritePolicy updateOnly; protected final ErrorHandler errorHandler; protected final TypeReference typeReference; + protected final AerospikeStoreSetting storeSetting; protected AerospikeStore(final IAerospikeClient client, final NamespaceSet namespaceSet, final ObjectMapper mapper, final Class clazz, final ErrorHandler errorHandler) { - this(client, namespaceSet, mapper, clazz, errorHandler, true); + this(client, namespaceSet, mapper, clazz, errorHandler, null); } protected AerospikeStore(final IAerospikeClient client, @@ -64,13 +75,13 @@ protected AerospikeStore(final IAerospikeClient client, final ObjectMapper mapper, final Class clazz, final ErrorHandler errorHandler, - final boolean failOnCreateIfRecordExists) { + final AerospikeStoreSetting storeSetting) { this(client, namespaceSet, mapper, new TypeReference<>() { @Override public Type getType() { return clazz; } - }, errorHandler, failOnCreateIfRecordExists); + }, errorHandler, storeSetting); } protected AerospikeStore(final IAerospikeClient client, @@ -78,7 +89,7 @@ protected AerospikeStore(final IAerospikeClient client, final ObjectMapper mapper, final TypeReference typeReference, final ErrorHandler errorHandler) { - this(client, namespaceSet, mapper, typeReference, errorHandler, true); + this(client, namespaceSet, mapper, typeReference, errorHandler, null); } protected AerospikeStore(final IAerospikeClient client, @@ -86,17 +97,20 @@ protected AerospikeStore(final IAerospikeClient client, final ObjectMapper mapper, final TypeReference typeReference, final ErrorHandler errorHandler, - final boolean failOnCreateIfRecordExists) { + final AerospikeStoreSetting storeSetting) { this.client = client; this.namespaceSet = namespaceSet; this.mapper = mapper; this.errorHandler = errorHandler; this.typeReference = typeReference; this.createPolicy = new WritePolicy(client.getWritePolicyDefault()); - createPolicy.recordExistsAction = failOnCreateIfRecordExists ? RecordExistsAction.CREATE_ONLY - : RecordExistsAction.REPLACE; + this.storeSetting = defaultIfNull(storeSetting, namespaceSet.set()); + createPolicy.recordExistsAction = this.storeSetting.failOnCreateIfRecordExists() + ? RecordExistsAction.CREATE_ONLY + : RecordExistsAction.REPLACE; this.updateOnly = new WritePolicy(client.getWritePolicyDefault()); updateOnly.recordExistsAction = RecordExistsAction.UPDATE_ONLY; + setupIndexes(); } /** @@ -143,7 +157,7 @@ public void update(final T item) { @Override public void delete(final String id) { exec("delete", id, () -> { - final Key requestIdKey = new Key(namespaceSet.namespace(), namespaceSet.set(), id); + final var requestIdKey = new Key(namespaceSet.namespace(), namespaceSet.set(), id); if (!client.delete(updateOnly, requestIdKey)) { errorHandler.onDeleteUnsuccessful(); } @@ -160,8 +174,8 @@ public void delete(final String id) { @Override public Optional get(final String id) { final T data = exec("get", id, () -> { - final Key requestIdKey = new Key(namespaceSet.namespace(), namespaceSet.set(), id); - final Record asRecord = client.get(client.getReadPolicyDefault(), requestIdKey); + final var requestIdKey = new Key(namespaceSet.namespace(), namespaceSet.set(), id); + final var asRecord = client.get(client.getReadPolicyDefault(), requestIdKey); return extractItem(id, asRecord); }, errorHandler); if (isValidDataItem(data)) { @@ -180,7 +194,7 @@ public Optional get(final String id) { */ @Override public Map get(final List ids) { - final Key[] batchReads = ids.stream() + final var batchReads = ids.stream() .map(id -> (new Key(namespaceSet.namespace(), namespaceSet.set(), id))) .toArray(Key[]::new); final Record[] records = client.get(client.getBatchPolicyDefault(), batchReads); @@ -212,15 +226,40 @@ public List list() { return items; } + @Override + public List getByRefId(final String refId) { + final var statement = new Statement(); + statement.setNamespace(namespaceSet.namespace()); + statement.setSetName(namespaceSet.set()); + statement.setIndexName(storeSetting.refIdIndex()); + statement.setFilter(Filter.contains(storeSetting.refIdBin(), IndexCollectionType.LIST, refId)); + final List results = new ArrayList<>(); + try (final RecordSet recordSet = client.query(client.getQueryPolicyDefault(), statement)) { + while (recordSet.next()) { + final T t = extractItem(recordSet.getKey().userKey.toString(), recordSet.getRecord()); + if (isValidDataItem(t)) { + results.add(t); + } else { + log.warn("Invalid item found for id:{}", t.id()); + } + } + } catch (AerospikeException e) { + log.error("[{}] Aerospike Error {} item for id:{}", typeReference.getType().getTypeName(), "getByRefId", refId, e); + return errorHandler.onAerospikeErrorForRefId(refId, e); + } + return results; + } + public T extractItem(String id, Record asRecord) { if (asRecord == null) { return errorHandler.onNoRecordFound(id); } - val data = asRecord.getString(DATA); + final var data = asRecord.getString(storeSetting.dataBin()); try { return mapper.readValue(data, typeReference); } catch (JsonProcessingException e) { - log.error("[{}] Deserialization error id:{} record:{}", typeReference.getType().getTypeName(), id, asRecord, e); + log.error("[{}] Deserialization error id:{} record:{}", typeReference.getType().getTypeName(), id, asRecord, + e); return errorHandler.onDeSerializationError(id, e); } } @@ -229,17 +268,23 @@ public Optional extractItemForBulkOperations(String id, Record asRecord) { if (asRecord == null) { return Optional.ofNullable(errorHandler.onNoRecordFound(id)); } - val data = asRecord.getString(DATA); + final var data = asRecord.getString(storeSetting.dataBin()); try { return Optional.of(mapper.readValue(data, typeReference)); } catch (JsonProcessingException e) { - log.error("[{}] Deserialization error id:{} record:{}", typeReference.getType().getTypeName(), id, asRecord, e); + log.error("[{}] Deserialization error id:{} record:{}", typeReference.getType().getTypeName(), id, asRecord, + e); return Optional.ofNullable(errorHandler.onDeSerializationError(id, e)); } } protected RecordDetails recordDetails(T item) throws JsonProcessingException { - final Bin dataBin = new Bin(DATA, mapper.writeValueAsString(item)); + final var dataBin = new Bin(storeSetting.dataBin(), mapper.writeValueAsString(item)); + final var refIds = item.refIds(); + if (refIds.isPresent()) { + final var refIdBin = new Bin(storeSetting.refIdBin(), refIds.get()); + return new RecordDetails(expiration(item), dataBin, refIdBin); + } return new RecordDetails(expiration(item), dataBin); } @@ -269,17 +314,56 @@ protected R exec(final String operation, final String id, ESupplier respo } private void write(final T item, WritePolicy defaultWritePolicy) { - val id = item.id(); + final var id = item.id(); exec("operation:" + defaultWritePolicy.recordExistsAction, id, () -> { - val recordDetails = recordDetails(item); + final var recordDetails = recordDetails(item); var writePolicy = defaultWritePolicy; if (recordDetails.expiration() > 0) { writePolicy = new WritePolicy(writePolicy); writePolicy.expiration = recordDetails.expiration(); } - val requestIdKey = new Key(namespaceSet.namespace(), namespaceSet.set(), id); + final var requestIdKey = new Key(namespaceSet.namespace(), namespaceSet.set(), id); client.put(writePolicy, requestIdKey, recordDetails.bins()); return null; }, errorHandler); } + + private void setupIndexes() { + try { + final var refIdIndexTask = client + .createIndex(null, namespaceSet.namespace(), namespaceSet.set(), + storeSetting.refIdIndex(), storeSetting.refIdBin(), + IndexType.STRING, + IndexCollectionType.LIST); + if (refIdIndexTask != null) { + refIdIndexTask.waitTillComplete(); + log.info("Created index: {}", storeSetting.refIdIndex()); + } + } catch (AerospikeException e) { + if (e.getResultCode() == 100) { + log.info("Index already exists:{}", storeSetting.refIdIndex()); + return; + } + log.error("Error while creating index:{}", storeSetting.refIdIndex(), e); + throw e; + } + } + + private AerospikeStoreSetting defaultIfNull(final AerospikeStoreSetting storeSetting, + final @NotNull @NotEmpty String set) { + final var builder = AerospikeStoreSetting.builder(); + if (storeSetting == null) { + return builder + .dataBin(DEFAULT_DATA_BIN) + .refIdBin(DEFAULT_REF_ID_BIN) + .failOnCreateIfRecordExists(true) + .refIdIndex(set + DEFAULT_REF_ID_INDEX_SUFFIX).build(); + } + builder.dataBin(Objects.requireNonNullElse(storeSetting.dataBin(), DEFAULT_DATA_BIN)); + final var refIdBin = Objects.requireNonNullElse(storeSetting.refIdBin(), DEFAULT_REF_ID_BIN); + builder.refIdBin(refIdBin); + builder.refIdIndex(Objects.requireNonNullElse(storeSetting.refIdIndex(), + set + "_" + refIdBin + DEFAULT_REF_ID_INDEX_SUFFIX)); + return builder.build(); + } } diff --git a/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeStoreSetting.java b/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeStoreSetting.java new file mode 100644 index 0000000..b98e98d --- /dev/null +++ b/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/AerospikeStoreSetting.java @@ -0,0 +1,11 @@ +package com.livetheoogway.crudstore.aerospike; + +import lombok.Builder; + +@Builder +public record AerospikeStoreSetting( + boolean failOnCreateIfRecordExists, + String dataBin, + String refIdBin, + String refIdIndex) { +} diff --git a/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/ErrorHandler.java b/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/ErrorHandler.java index 2cb2d82..3a83d90 100644 --- a/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/ErrorHandler.java +++ b/aerospike-crud-store/src/main/java/com/livetheoogway/crudstore/aerospike/ErrorHandler.java @@ -17,6 +17,8 @@ import com.aerospike.client.AerospikeException; import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.List; + @SuppressWarnings("java:S112") public interface ErrorHandler { void onDeleteUnsuccessful(); @@ -27,6 +29,10 @@ public interface ErrorHandler { T onAerospikeError(String id, AerospikeException e); + default List onAerospikeErrorForRefId(String id, AerospikeException e) { + return List.of(); + } + T onSerializationError(final String id, JsonProcessingException e); T onExecutionError(final String id, Exception e); diff --git a/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/AerospikeStoreTest.java b/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/AerospikeStoreTest.java index f6c768f..73d5cdb 100644 --- a/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/AerospikeStoreTest.java +++ b/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/AerospikeStoreTest.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.livetheoogway.crudstore.aerospike.data.ProfileData; import com.livetheoogway.crudstore.aerospike.data.UserData; +import com.livetheoogway.crudstore.aerospike.data.UserDataWithReferences; import com.livetheoogway.crudstore.aerospike.stores.TestTypeRefAerospikeStore; import com.livetheoogway.crudstore.aerospike.stores.UserAerospikeReplaceStore; import com.livetheoogway.crudstore.aerospike.stores.UserAerospikeStore; @@ -127,6 +128,81 @@ void testStoreOperationsForTypeRef() { }); } + @Test + void testStoreOperationsForDataWithReferences() { + + final TestTypeRefAerospikeStore store + = new TestTypeRefAerospikeStore<>(aerospikeClient, + new NamespaceSet("test", "test-4"), + new ObjectMapper(), + new TypeReference<>() {}, + new DefaultErrorHandler<>()); + + final var me = UserDataWithReferences.builder() + .id("EMP001") + .name("Tushar") + .references(List.of("Smart", "Handsome")) + .build(); + + final var meUpdated = UserDataWithReferences.builder() + .id("EMP001") + .name("Tushar Revamped") + .references(List.of("Smart", "Handsome")) + .build(); + + final var you = UserDataWithReferences.builder() + .id("EMP002") + .name("Pickle Rick") + .references(List.of("Cucumber", "Ugly")) + .build(); + + TestUtils.testStoreOperations(store, + () -> me, + () -> meUpdated, + () -> you, + () -> UserDataWithReferences.builder().id("unknown").build(), + AerospikeStoreTest::checkIfMatches); + var result = store.getByRefId("Smart"); + checkIfMatches(meUpdated, result.stream().findFirst()); + result = store.getByRefId("Handsome"); + checkIfMatches(meUpdated, result.stream().findFirst()); + + /* get by unknown ref */ + result = store.getByRefId("random"); + assertTrue(result.isEmpty()); + + /* test if multiple records match */ + store.create(UserDataWithReferences.builder() + .id("EMP003") + .name("Morty") + .references(List.of("Stupid", "Smart")) + .build()); + result = store.getByRefId("Smart"); + assertEquals(2, result.size()); + assertTrue(result.stream().anyMatch(data -> data.id().equals("EMP001"))); + assertTrue(result.stream().anyMatch(data -> data.id().equals("EMP003"))); + + /* test if multiple records match */ + store.create(UserDataWithReferences.builder() + .id("EMP004") + .name("Summer") + .references(List.of("Stupid", "Smart", "Smart")) + .build()); + result = store.getByRefId("Smart"); + assertEquals(3, result.size()); + assertTrue(result.stream().anyMatch(data -> data.id().equals("EMP001"))); + assertTrue(result.stream().anyMatch(data -> data.id().equals("EMP003"))); + assertTrue(result.stream().anyMatch(data -> data.id().equals("EMP004"))); + } + + private static boolean checkIfMatches(final UserDataWithReferences data, final Optional result) { + assertTrue(result.isPresent()); + assertEquals(data.id(), result.get().id()); + assertEquals(data.name(), result.get().name()); + assertEquals(data.references(), result.get().references()); + return true; + } + @Test void testStoreOperationsOnReplace() { diff --git a/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/data/UserDataWithReferences.java b/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/data/UserDataWithReferences.java new file mode 100644 index 0000000..9cc38e6 --- /dev/null +++ b/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/data/UserDataWithReferences.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022. Live the Oogway, Tushar Naik + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ + +package com.livetheoogway.crudstore.aerospike.data; + +import com.livetheoogway.crudstore.core.Id; +import lombok.Builder; + +import java.util.List; +import java.util.Optional; + +@Builder +public record UserDataWithReferences( + String id, + String name, + List references) implements Id { + @Override + public Optional> refIds() { + return Optional.of(references); + } +} diff --git a/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/stores/UserAerospikeReplaceStore.java b/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/stores/UserAerospikeReplaceStore.java index 658fad9..f93c51b 100644 --- a/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/stores/UserAerospikeReplaceStore.java +++ b/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/stores/UserAerospikeReplaceStore.java @@ -17,6 +17,7 @@ import com.aerospike.client.IAerospikeClient; import com.fasterxml.jackson.databind.ObjectMapper; import com.livetheoogway.crudstore.aerospike.AerospikeStore; +import com.livetheoogway.crudstore.aerospike.AerospikeStoreSetting; import com.livetheoogway.crudstore.aerospike.ErrorHandler; import com.livetheoogway.crudstore.aerospike.NamespaceSet; import com.livetheoogway.crudstore.aerospike.data.UserData; @@ -27,6 +28,7 @@ public UserAerospikeReplaceStore(final IAerospikeClient client, final NamespaceSet namespaceSet, final ObjectMapper mapper, final ErrorHandler errorHandler) { - super(client, namespaceSet, mapper, UserData.class, errorHandler, false); + super(client, namespaceSet, mapper, UserData.class, errorHandler, AerospikeStoreSetting.builder() + .failOnCreateIfRecordExists(false).build()); } } diff --git a/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/util/DataUtils.java b/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/util/DataUtils.java index 4230fbf..33cbfdb 100644 --- a/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/util/DataUtils.java +++ b/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/util/DataUtils.java @@ -1,3 +1,17 @@ +/* + * Copyright 2022. Live the Oogway, Tushar Naik + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ + package com.livetheoogway.crudstore.aerospike.util; import com.livetheoogway.crudstore.aerospike.data.ProfileData; diff --git a/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/util/TestUtils.java b/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/util/TestUtils.java index 3d54e7d..7b71b76 100644 --- a/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/util/TestUtils.java +++ b/aerospike-crud-store/src/test/java/com/livetheoogway/crudstore/aerospike/util/TestUtils.java @@ -1,3 +1,17 @@ +/* + * Copyright 2022. Live the Oogway, Tushar Naik + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and limitations + * under the License. + */ + package com.livetheoogway.crudstore.aerospike.util; import com.livetheoogway.crudstore.aerospike.AerospikeStore; @@ -18,6 +32,17 @@ @UtilityClass public class TestUtils { + /** + * Test basic store operations + * + * @param store store to test + * @param initialData initial data + * @param updatedData data after update + * @param anotherData another data to test bulk get + * @param unknownData unknown data to test update fails when create wasnt done + * @param validator validator to check if data from store is correct + * @param type of data + */ public void testStoreOperations(final AerospikeStore store, final Supplier initialData, final Supplier updatedData, diff --git a/crud-store-core/src/main/java/com/livetheoogway/crudstore/core/Id.java b/crud-store-core/src/main/java/com/livetheoogway/crudstore/core/Id.java index 435c10c..66c2863 100644 --- a/crud-store-core/src/main/java/com/livetheoogway/crudstore/core/Id.java +++ b/crud-store-core/src/main/java/com/livetheoogway/crudstore/core/Id.java @@ -14,6 +14,13 @@ package com.livetheoogway.crudstore.core; +import java.util.List; +import java.util.Optional; + public interface Id { String id(); + + default Optional> refIds() { + return Optional.empty(); + } } diff --git a/crud-store-core/src/main/java/com/livetheoogway/crudstore/core/Store.java b/crud-store-core/src/main/java/com/livetheoogway/crudstore/core/Store.java index 68cff56..4eea684 100644 --- a/crud-store-core/src/main/java/com/livetheoogway/crudstore/core/Store.java +++ b/crud-store-core/src/main/java/com/livetheoogway/crudstore/core/Store.java @@ -30,4 +30,8 @@ public interface Store { Map get(final List ids); List list(); + + default List getByRefId(final String refId) { + throw new UnsupportedOperationException("Not implemented"); + } } diff --git a/crud-store-core/src/test/java/com/livetheoogway/crudstore/core/CachingStoreTest.java b/crud-store-core/src/test/java/com/livetheoogway/crudstore/core/CachingStoreTest.java index 9f3f1d1..9f7759c 100644 --- a/crud-store-core/src/test/java/com/livetheoogway/crudstore/core/CachingStoreTest.java +++ b/crud-store-core/src/test/java/com/livetheoogway/crudstore/core/CachingStoreTest.java @@ -23,7 +23,10 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class CachingStoreTest { @@ -113,4 +116,10 @@ void testExpireAndRefreshAfterWrite() { assertEquals("four too", testData.get().name()); assertEquals(7, testData.get().age()); } + + @Test + void testUnsupportedErrorOnGetByRefId() { + Store store = new CachingStore<>(new InMemoryStore<>(), 5, 2, 1); + assertThrows(UnsupportedOperationException.class, () -> store.getByRefId("some")); + } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index f06913e..0436ada 100644 --- a/pom.xml +++ b/pom.xml @@ -181,7 +181,8 @@ https://sonarcloud.io ${project.groupId}:${project.artifactId} - **com/livetheoogway/crudstore/aerospike/RecordDetails.java + **com/livetheoogway/crudstore/aerospike/RecordDetails.java, + **com/livetheoogway/crudstore/aerospike/AerospikeClientHelpers.java,