Skip to content

Commit

Permalink
Support the Size limitation in the InMemory cache. (#1070)
Browse files Browse the repository at this point in the history
* Tested Reject eviction mode set as default.

Signed-off-by: Jakub Balhar <[email protected]>

* Move the sizing of the InMemory on the per record size

Signed-off-by: Jakub Balhar <[email protected]>

* Test the reject eviction strategy in the integration tests

Signed-off-by: Jakub Balhar <[email protected]>

* The preparations for the other eviction strategies

Signed-off-by: Jakub Balhar <[email protected]>

* Clean code smells

Signed-off-by: Jakub Balhar <[email protected]>

Co-authored-by: Jakub Balhar <[email protected]>
  • Loading branch information
balhar-jakub and balhar-jakub authored Jan 8, 2021
1 parent 12b60ab commit e3aff47
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public enum Messages {
DUPLICATE_KEY("org.zowe.apiml.cache.keyCollision", HttpStatus.CONFLICT),
KEY_NOT_PROVIDED("org.zowe.apiml.cache.keyNotProvided", HttpStatus.BAD_REQUEST),
KEY_NOT_IN_CACHE("org.zowe.apiml.cache.keyNotInCache", HttpStatus.NOT_FOUND),
INVALID_PAYLOAD("org.zowe.apiml.cache.invalidPayload", HttpStatus.BAD_REQUEST);
INVALID_PAYLOAD("org.zowe.apiml.cache.invalidPayload", HttpStatus.BAD_REQUEST),
INSUFFICIENT_STORAGE("org.zowe.apiml.cache.insufficientStorage", HttpStatus.INSUFFICIENT_STORAGE);

private final String key;
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.zowe.apiml.caching.service.Messages;
import org.zowe.apiml.caching.service.Storage;
import org.zowe.apiml.caching.service.StorageException;
import org.zowe.apiml.caching.service.inmemory.config.InMemoryConfig;

import java.util.HashMap;
import java.util.Map;
Expand All @@ -22,11 +23,14 @@
@Slf4j
public class InMemoryStorage implements Storage {
private Map<String, Map<String, KeyValue>> storage = new ConcurrentHashMap<>();
private InMemoryConfig inMemoryConfig;

public InMemoryStorage() {
public InMemoryStorage(InMemoryConfig inMemoryConfig) {
this.inMemoryConfig = inMemoryConfig;
}

protected InMemoryStorage(Map<String, Map<String, KeyValue>> storage) {
protected InMemoryStorage(InMemoryConfig inMemoryConfig, Map<String, Map<String, KeyValue>> storage) {
this(inMemoryConfig);
this.storage = storage;
}

Expand All @@ -39,7 +43,14 @@ public KeyValue create(String serviceId, KeyValue toCreate) {
if (serviceStorage.containsKey(toCreate.getKey())) {
throw new StorageException(Messages.DUPLICATE_KEY.getKey(), Messages.DUPLICATE_KEY.getStatus(), toCreate.getKey());
}

String evictionStrategy = inMemoryConfig.getGeneralConfig().getEvictionStrategy();
if (evictionStrategy.equals("reject")) {
verifyTotalSize(toCreate.getKey());
}

serviceStorage.put(toCreate.getKey(), toCreate);

return toCreate;
}

Expand All @@ -65,6 +76,7 @@ public KeyValue update(String serviceId, KeyValue toUpdate) {
}

Map<String, KeyValue> serviceStorage = storage.get(serviceId);

serviceStorage.put(key, toUpdate);
return toUpdate;
}
Expand All @@ -90,4 +102,17 @@ private boolean isKeyNotInCache(String serviceId, String keyToTest) {
Map<String, KeyValue> serviceSpecificStorage = storage.get(serviceId);
return serviceSpecificStorage == null || serviceSpecificStorage.get(keyToTest) == null;
}

private void verifyTotalSize(String key) {
int currentSize = 0;
for (Map.Entry<String, Map<String, KeyValue>> serviceStorage: storage.entrySet()) {
currentSize += serviceStorage.getValue().size();
}

log.info("Current Size {}.", currentSize);

if (currentSize >= inMemoryConfig.getMaxDataSize()) {
throw new StorageException(Messages.INSUFFICIENT_STORAGE.getKey(), Messages.INSUFFICIENT_STORAGE.getStatus(), key);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
public class InMemoryConfig {
private final GeneralConfig generalConfig;

@Value("${caching.storage.inmemory.size:10000}")
@Value("${caching.storage.inmemory.size:100}")
private int maxDataSize;

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
@Configuration
@RequiredArgsConstructor
public class InMemoryConfiguration {
private final InMemoryConfig inMemoryConfig;

@ConditionalOnProperty(name = "caching.storage.mode", matchIfMissing = true)
@ConditionalOnProperty(name = "caching.storage.mode", havingValue = "inMemory", matchIfMissing = true)
@Bean
public Storage inMemory() {
return new InMemoryStorage();
return new InMemoryStorage(inMemoryConfig);
}
}
8 changes: 8 additions & 0 deletions caching-service/src/main/resources/caching-log-messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ messages:
reason: "Key is already in the cache."
action: "Update or delete the key, or add a different key."

- key: org.zowe.apiml.cache.insufficientStorage
number: ZWECS134
type: ERROR
text: "Insufficient storage space limit. Key '%s' cannot be added in the cache."
reason: "The storage space is full."
action: "Disable the 'rejected' eviction strategy."


# Service specific messages (700 - 799)
- key: org.zowe.apiml.cache.gatewayUnavailable
number: ZWECS700
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.zowe.apiml.caching.config.GeneralConfig;
import org.zowe.apiml.caching.model.KeyValue;
import org.zowe.apiml.caching.service.StorageException;
import org.zowe.apiml.caching.service.inmemory.config.InMemoryConfig;

import java.util.HashMap;
import java.util.Map;
Expand All @@ -23,19 +25,24 @@

public class InMemoryStorageTest {
private InMemoryStorage underTest;
private InMemoryConfig config;

private Map<String, Map<String, KeyValue>> testingStorage;
private final String serviceId = "acme";

@BeforeEach
void setUp() {
testingStorage = new HashMap<>();
underTest = new InMemoryStorage(testingStorage);
GeneralConfig generalConfig = new GeneralConfig();
generalConfig.setEvictionStrategy("reject");
config = new InMemoryConfig(generalConfig);
config.setMaxDataSize(10);
underTest = new InMemoryStorage(config, testingStorage);
}

@Test
void givenDefaultStorageConstructor_whenStorageConstructed_thenCanUseStorage() {
underTest = new InMemoryStorage();
underTest = new InMemoryStorage(config);
underTest.create(serviceId, new KeyValue("key", "value"));

KeyValue result = underTest.read(serviceId, "key");
Expand Down Expand Up @@ -133,4 +140,19 @@ void givenKeyExists_whenDeletionRequested_thenKeyValueIsReturnedAndKeyIsRemoved(
underTest.delete(serviceId, "username");
assertThat(serviceStorage.containsKey("username"), is(false));
}

@Test
void givenTheStorageIsFull_whenNewKeyValueIsAdded_thenTheInsufficientStorageExceptionIsRaised() {
GeneralConfig generalConfig = new GeneralConfig();
generalConfig.setEvictionStrategy("reject");
config = new InMemoryConfig(generalConfig);
config.setMaxDataSize(1);

underTest = new InMemoryStorage(config);
underTest.create("customService", new KeyValue("key", "willFit"));
KeyValue wontFit = new KeyValue("key", "wontFit");
assertThrows(StorageException.class, () -> {
underTest.create(serviceId, wontFit);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/
package org.zowe.apiml.cachingservice;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.restassured.RestAssured;
import lombok.Data;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.zowe.apiml.gatewayservice.SecurityUtils;
import org.zowe.apiml.util.categories.NotForMainframeTest;
import org.zowe.apiml.util.http.HttpRequestUtils;

import java.net.URI;

import static io.restassured.RestAssured.given;
import static io.restassured.http.ContentType.JSON;
import static org.apache.http.HttpStatus.SC_CREATED;
import static org.apache.http.HttpStatus.SC_INSUFFICIENT_STORAGE;
import static org.hamcrest.core.Is.is;

@NotForMainframeTest // Remove later when implemented for VSAM as well.
class RejectEvictionTest {
private static final URI CACHING_PATH = HttpRequestUtils.getUriFromGateway("/cachingservice/api/v1/cache");
private final static String COOKIE_NAME = "apimlAuthenticationToken";
private static String jwtToken = SecurityUtils.gatewayToken();

@BeforeAll
static void setup() {
RestAssured.useRelaxedHTTPSValidation();
}

@Test
void givenStorageIsFull_whenAnotherKeyIsInserted_thenItIsRejected() {
int amountOfAllowedRecords = 100;
try {
KeyValue keyValue;

// The default configuration is to allow 100 records.
for (int i = 0; i < amountOfAllowedRecords; i++) {
keyValue = new KeyValue("key" + i, "testValue");
create(keyValue);
}

keyValue = new KeyValue("keyThatWontPass", "testValue");
given()
.contentType(JSON)
.body(keyValue)
.cookie(COOKIE_NAME, jwtToken)
.when()
.post(CACHING_PATH)
.then()
.statusCode(is(SC_INSUFFICIENT_STORAGE));
} finally {
for (int i = 0; i < amountOfAllowedRecords; i++) {
deteleValueUnderServiceIdWithoutValidation("key" + i, jwtToken);
}
}
}

private void create(KeyValue keyValue) {
given()
.contentType(JSON)
.body(keyValue)
.cookie(COOKIE_NAME, jwtToken)
.when()
.post(CACHING_PATH)
.then()
.statusCode(is(SC_CREATED));
}

private static void deteleValueUnderServiceIdWithoutValidation(String value, String jwtToken) {
given()
.contentType(JSON)
.cookie(COOKIE_NAME, jwtToken)
.when()
.delete(CACHING_PATH + "/" + value);
}

@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Data
private static class KeyValue {
private final String key;
private final String value;

@JsonCreator
public KeyValue(String key, String value) {
this.key = key;
this.value = value;
}
}
}

0 comments on commit e3aff47

Please sign in to comment.