Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: support if-none-match=* header in resource PUT API #191

Merged
merged 2 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ FROM eclipse-temurin:17-jdk-alpine
# TODO remove the fix once a new version is released
RUN apk update && apk upgrade --no-cache libcrypto3 libssl3

ENV JAVA_OPTS="-Dgflog.config=/app/config/gflog.xml"
ENV OTEL_TRACES_EXPORTER="none"
ENV OTEL_METRICS_EXPORTER="none"
ENV OTEL_LOGS_EXPORTER="none"
Expand All @@ -27,7 +26,6 @@ WORKDIR /app
RUN adduser -u 1001 --disabled-password --gecos "" appuser

COPY --from=builder --chown=appuser:appuser /build/ .
COPY --chown=appuser:appuser ./config/* /app/config/
RUN mkdir /app/log && chown -R appuser:appuser /app

USER appuser
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ Static settings are used on startup and cannot be changed while application is r
| storage.createBucket | false | Indicates whether bucket should be created on start-up
| encryption.password | - | Password used for AES encryption
| encryption.salt | - | Salt used for AES encryption
| encryption.salt | - | Salt used for AES encryption
| resources.maxSize | 1048576 | Max allowed size in bytes for a resource
| resources.syncPeriod | 60000 | Period in milliseconds, how frequently check for resources to sync
| resources.syncDelay | 120000 | Delay in milliseconds for a resource to be written back in object storage after last modification
Expand All @@ -76,7 +75,7 @@ maxmemory 4G
maxmemory-policy volatile-lfu
```

Note: Redis will be strictly required in the upcoming releases 0.7+.
Note: Redis will be strictly required in the upcoming releases 0.8+.

### Dynamic settings

Expand Down
17 changes: 0 additions & 17 deletions config/gflog.xml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,11 @@ private void load(boolean fail) {
}

private Config loadConfig() throws Exception {
JsonNode tree = null;
JsonNode tree = ProxyUtil.MAPPER.createObjectNode();

for (String path : paths) {
try (InputStream stream = openStream(path)) {
if (tree == null) {
tree = ProxyUtil.MAPPER.readTree(stream);
} else {
tree = ProxyUtil.MAPPER.readerForUpdating(tree).readTree(stream);
}
tree = ProxyUtil.MAPPER.readerForUpdating(tree).readTree(stream);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.epam.aidial.core.util.ProxyUtil;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpMethod;
import lombok.extern.slf4j.Slf4j;

Expand Down Expand Up @@ -106,16 +107,29 @@ private Future<?> putResource(ResourceDescription descriptor) {
return context.respond(HttpStatus.REQUEST_ENTITY_TOO_LARGE, message);
}

String ifNoneMatch = context.getRequest().getHeader(HttpHeaders.IF_NONE_MATCH);
boolean overwrite = (ifNoneMatch == null);

if (ifNoneMatch != null && !ifNoneMatch.equals("*")) {
return context.respond(HttpStatus.BAD_REQUEST, "only header if-none-match=* is supported");
}

return context.getRequest().body().compose(bytes -> {
if (bytes.length() > contentLimit) {
String message = "Resource size: %s exceeds max limit: %s".formatted(bytes.length(), contentLimit);
throw new HttpException(HttpStatus.REQUEST_ENTITY_TOO_LARGE, message);
}

String body = bytes.toString(StandardCharsets.UTF_8);
return vertx.executeBlocking(() -> service.putResource(descriptor, body));
return vertx.executeBlocking(() -> service.putResource(descriptor, body, overwrite));
})
.onSuccess((metadata) -> {
if (metadata == null) {
context.respond(HttpStatus.CONFLICT, "Resource already exists: " + descriptor.getUrl());
} else {
context.respond(HttpStatus.OK, metadata);
}
})
.onSuccess((metadata) -> context.respond(HttpStatus.OK, metadata))
.onFailure(error -> {
if (error instanceof HttpException exception) {
context.respond(exception.getStatus(), exception.getMessage());
Expand Down
23 changes: 14 additions & 9 deletions src/main/java/com/epam/aidial/core/service/ResourceService.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ public ResourceService(Vertx vertx,
}

/**
* @param maxSize - max allowed size in bytes for a resource.
* @param syncPeriod - period in milliseconds, how frequently check for resources to sync.
* @param syncDelay - delay in milliseconds for a resource to be written back in object storage after last modification.
* @param syncBatch - how many resources to sync in one go.
* @param cacheExpiration - expiration in milliseconds for synced resources in Redis.
* @param maxSize - max allowed size in bytes for a resource.
* @param syncPeriod - period in milliseconds, how frequently check for resources to sync.
* @param syncDelay - delay in milliseconds for a resource to be written back in object storage after last modification.
* @param syncBatch - how many resources to sync in one go.
* @param cacheExpiration - expiration in milliseconds for synced resources in Redis.
* @param compressionMinSize - compress resources with gzip if their size in bytes more or equal to this value.
*/
public ResourceService(Vertx vertx,
Expand Down Expand Up @@ -192,11 +192,12 @@ public String getResource(ResourceDescription descriptor, boolean lock) {
return result.exists ? result.body : null;
}

public ResourceItemMetadata putResource(ResourceDescription descriptor, String body) {
return putResource(descriptor, body, true);
public ResourceItemMetadata putResource(ResourceDescription descriptor, String body, boolean overwrite) {
return putResource(descriptor, body, overwrite, true);
}

public ResourceItemMetadata putResource(ResourceDescription descriptor, String body, boolean lock) {
public ResourceItemMetadata putResource(ResourceDescription descriptor, String body,
boolean overwrite, boolean lock) {
String redisKey = redisKey(descriptor);
String blobKey = blobKey(descriptor);

Expand All @@ -206,6 +207,10 @@ public ResourceItemMetadata putResource(ResourceDescription descriptor, String b
result = blobGet(blobKey, false);
}

if (result.exists && !overwrite) {
return null;
}

long updatedAt = time();
long createdAt = result.exists ? result.createdAt : updatedAt;
redisPut(redisKey, new Result(body, createdAt, updatedAt, false, true));
Expand All @@ -224,7 +229,7 @@ public void computeResource(ResourceDescription descriptor, Function<String, Str
try (var ignore = lockService.lock(redisKey)) {
String body = getResource(descriptor, false);
String updatedBody = fn.apply(body);
putResource(descriptor, updatedBody, false);
putResource(descriptor, updatedBody, true, false);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/epam/aidial/core/util/HttpStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum HttpStatus {
FORBIDDEN(403),
NOT_FOUND(404),
METHOD_NOT_ALLOWED(405),
CONFLICT(409),
REQUEST_ENTITY_TOO_LARGE(413),
UNSUPPORTED_MEDIA_TYPE(415),
UNPROCESSABLE_ENTITY(422),
Expand Down
21 changes: 18 additions & 3 deletions src/test/java/com/epam/aidial/core/ResourceApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ void testWorkflow() {
response = request(HttpMethod.PUT, "/folder/conversation", "12345");
verifyNotExact(response, 200, "\"url\":\"conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation\"");

response = request(HttpMethod.PUT, "/folder/conversation", "12345", "if-none-match", "*");
verifyNotExact(response, 409, "Resource already exists: conversations/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/folder/conversation");

response = request(HttpMethod.GET, "/folder/conversation");
verify(response, 200, "12345");

Expand Down Expand Up @@ -165,6 +168,12 @@ void testLimit() {
verify(response, 413, "Resource size: 1048577 exceeds max limit: 1048576");
}

@Test
void testUnsupportedIfNoneMatchHeader() {
Response response = request(HttpMethod.PUT, "/folder/big", "1", "if-none-match", "unsupported");
verify(response, 400, "only header if-none-match=* is supported");
}

@Test
void testRandom() {
ThreadLocalRandom random = ThreadLocalRandom.current();
Expand Down Expand Up @@ -226,16 +235,16 @@ private Response request(HttpMethod method, String resource) {
return request(method, resource, "");
}

private Response request(HttpMethod method, String resource, String body) {
return send(method, "/v1/conversations/" + bucket + resource, body);
private Response request(HttpMethod method, String resource, String body, String... headers) {
return send(method, "/v1/conversations/" + bucket + resource, body, headers);
}

private Response metadata(String resource) {
return send(HttpMethod.GET, "/v1/metadata/conversations/" + bucket + resource, "");
}

@SneakyThrows
private Response send(HttpMethod method, String path, String body) {
private Response send(HttpMethod method, String path, String body, String... headers) {
String uri = "http://127.0.0.1:" + dial.getServer().actualPort() + path;
HttpUriRequest request;

Expand All @@ -253,6 +262,12 @@ private Response send(HttpMethod method, String path, String body) {

request.addHeader("api-key", "proxyKey1");

for (int i = 0; i < headers.length; i += 2) {
String key = headers[i];
String value = headers[i + 1];
request.addHeader(key, value);
}

try (CloseableHttpResponse response = client.execute(request)) {
int status = response.getStatusLine().getStatusCode();
String answer = EntityUtils.toString(response.getEntity());
Expand Down
Loading