Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ af9205fa592e4ae32ceca951c560a48b512b7744
f58dd73747791edcd45722a4903da44da1b9b2d3
fcdaa3d6735d2382a44d066a6ce051a2c5e6469f
9bbe16b7d1600fc96100f54cb25869cbdf425521
3addaca346bcdfd555fca0340e40c7e5518c5185
21 changes: 21 additions & 0 deletions commercetools/commercetools-okhttp-client5/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apply plugin: "me.champeau.jmh"

jmh {
iterations = 5
benchmarkMode = ['thrpt']
threads = 25
fork = 3
timeOnIteration = '1s'
profilers = ['gc']
}

dependencies {
api project(":rmf:rmf-java-base")
api "com.squareup.okhttp3:okhttp:5.3.0" version {
strictly '[5.0,5.99999]'
prefer "5.3.0"
}
implementation "com.squareup.okio:okio:3.14.0"

implementation javax.validation
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@

package com.commercetools.http.okhttp4;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPOutputStream;

import io.vrap.rmf.base.client.ApiHttpResponse;

import org.assertj.core.api.Assertions;
import org.openjdk.jmh.annotations.*;

import okhttp3.*;
import okio.Okio;

public class UnzipBenchmark {

@State(Scope.Benchmark)
public static class InterceptorState {
private CtOkHttp4Client.UnzippingInterceptor interceptor;

@Setup(Level.Trial)
public void init() {
interceptor = new CtOkHttp4Client.UnzippingInterceptor();
printUsedMemory();

}

@TearDown(Level.Trial)
public void tearDown() {
printUsedMemory();
}
}
@Benchmark
public void unzip(InterceptorState state) throws IOException {

ByteArrayOutputStream os = new ByteArrayOutputStream();
GZIPOutputStream gzipOs = new GZIPOutputStream(os);
byte[] buffer = "Sample Text".getBytes();
gzipOs.write(buffer, 0, buffer.length);
gzipOs.close();
ByteArrayInputStream inputStream = new ByteArrayInputStream(os.toByteArray());

Response gzipped = new Response.Builder().request(new Request.Builder().url("http://localhost").build())
.protocol(Protocol.HTTP_1_1)
.addHeader("content-encoding", "gzip")
.addHeader("content-type", "application/json")
.code(200)
.message("Ok")
.body(ResponseBody.create(MediaType.parse("application/json"), -1L,
Okio.buffer(Okio.source(inputStream))))
.build();
Assertions.assertThat(gzipped.body().source().isOpen()).isTrue();

Response unzipped = state.interceptor.unzip(gzipped);

Assertions.assertThat(gzipped.body().source().isOpen()).isTrue();
Assertions.assertThat(unzipped.body().source().isOpen()).isTrue();
Assertions.assertThat(inputStream.available()).isEqualTo(31);

ApiHttpResponse<byte[]> response = CtOkHttp4Client.toResponse(unzipped);

Assertions.assertThat(gzipped.body().source().isOpen()).isFalse();
Assertions.assertThat(unzipped.body().source().isOpen()).isFalse();
Assertions.assertThat(inputStream.available()).isEqualTo(0);

String text = new String(response.getBody(), StandardCharsets.UTF_8);
Assertions.assertThat(text).isEqualTo("Sample Text");
}

public static void printUsedMemory() {
long _usedMem;
long _total;
long _total2;
long _count = -1;
System.out.flush();
// loop to get a stable reading, since memory may be resized between the method calls
do {
_count++;
_total = Runtime.getRuntime().totalMemory();
try {
Thread.sleep(12);
}
catch (Exception ignore) {
}
long _free = Runtime.getRuntime().freeMemory();
_total2 = Runtime.getRuntime().totalMemory();
_usedMem = _total - _free;
} while (_total != _total2);
System.out.println("before GC: used=" + _usedMem + ", loopCount=" + _count + ", total=" + _total);
try {
Runtime.getRuntime().gc();
Thread.sleep(55);
Runtime.getRuntime().gc();
Thread.sleep(55);
Runtime.getRuntime().gc();
Thread.sleep(55);
Runtime.getRuntime().gc();
Thread.sleep(55);
}
catch (Exception ignore) {
}
// loop to get a stable reading, since memory may be resized between the method calls
do {
_count++;
_total = Runtime.getRuntime().totalMemory();
try {
Thread.sleep(12);
}
catch (Exception ignore) {
}
long _free = Runtime.getRuntime().freeMemory();
_total2 = Runtime.getRuntime().totalMemory();
_usedMem = _total - _free;
} while (_total != _total2);
System.out.println("after GC: used=" + _usedMem + ", loopCount=" + _count + ", total=" + _total);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

package com.commercetools.http.okhttp4;

import okhttp3.OkHttpClient;

@FunctionalInterface
public interface BuilderOptions {
OkHttpClient.Builder plus(OkHttpClient.Builder builder);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@

package com.commercetools.http.okhttp4;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import io.vrap.rmf.base.client.*;
import io.vrap.rmf.base.client.utils.Utils;

import jakarta.validation.constraints.NotNull;
import okhttp3.OkHttpClient;
import okio.GzipSource;
import okio.Okio;

public class CtOkHttp4Client extends HttpClientBase {

public static final int MAX_REQUESTS = 64;
private final Supplier<OkHttpClient.Builder> clientBuilder = () -> new OkHttpClient.Builder()
.connectTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.protocols(Collections.singletonList(okhttp3.Protocol.HTTP_1_1))
.addInterceptor(new UnzippingInterceptor());

private final OkHttpClient okHttpClient;

public CtOkHttp4Client() {
super();
okHttpClient = clientBuilder.get().dispatcher(createDispatcher(MAX_REQUESTS, MAX_REQUESTS)).build();
}

public CtOkHttp4Client(final BuilderOptions options) {
super();
okHttpClient = options.plus(clientBuilder.get().dispatcher(createDispatcher(MAX_REQUESTS, MAX_REQUESTS)))
.build();
}

public CtOkHttp4Client(final Supplier<OkHttpClient.Builder> builderSupplier) {
super();
okHttpClient = builderSupplier.get().build();
}

public CtOkHttp4Client(final int maxRequests, final int maxRequestsPerHost) {
super();
okHttpClient = clientBuilder.get().dispatcher(createDispatcher(maxRequests, maxRequestsPerHost)).build();
}

public CtOkHttp4Client(final int maxRequests, final int maxRequestsPerHost, final BuilderOptions options) {
super();
okHttpClient = options.plus(clientBuilder.get().dispatcher(createDispatcher(maxRequests, maxRequestsPerHost)))
.build();
}

public CtOkHttp4Client(final ExecutorService executor) {
super(executor);
okHttpClient = clientBuilder.get().dispatcher(createDispatcher(executor, MAX_REQUESTS, MAX_REQUESTS)).build();
}

public CtOkHttp4Client(final ExecutorService executor, final BuilderOptions options) {
super(executor);
okHttpClient = options.plus(clientBuilder.get().dispatcher(createDispatcher(MAX_REQUESTS, MAX_REQUESTS)))
.build();
}

public CtOkHttp4Client(final ExecutorService executor, final int maxRequests, final int maxRequestsPerHost) {
super(executor);
okHttpClient = clientBuilder.get()
.dispatcher(createDispatcher(executor, maxRequests, maxRequestsPerHost))
.build();
}

public CtOkHttp4Client(final ExecutorService executor, final int maxRequests, final int maxRequestsPerHost,
final BuilderOptions options) {
super(executor);
okHttpClient = options
.plus(clientBuilder.get().dispatcher(createDispatcher(executor, maxRequests, maxRequestsPerHost)))
.build();
}

public okhttp3.Dispatcher createDispatcher(final int maxRequests, final int maxRequestsPerHost) {
final okhttp3.Dispatcher dispatcher = new okhttp3.Dispatcher();
dispatcher.setMaxRequests(maxRequests);
dispatcher.setMaxRequestsPerHost(maxRequestsPerHost);
return dispatcher;
}

public okhttp3.Dispatcher createDispatcher(final ExecutorService executor, final int maxRequests,
final int maxRequestsPerHost) {
final okhttp3.Dispatcher dispatcher = new okhttp3.Dispatcher(executor);
dispatcher.setMaxRequests(maxRequests);
dispatcher.setMaxRequestsPerHost(maxRequestsPerHost);
return dispatcher;
}

private static final String CONTENT_TYPE = "Content-Type";
private static final okhttp3.MediaType JSON = okhttp3.MediaType.get("application/json; charset=utf-8");
private static final byte[] emptyBody = new byte[0];

@Override
public CompletableFuture<ApiHttpResponse<byte[]>> execute(final ApiHttpRequest request) {
return makeRequest(okHttpClient, toRequest(request)).thenApplyAsync(CtOkHttp4Client::toResponse, executor());

}

static ApiHttpResponse<byte[]> toResponse(final okhttp3.Response response) {
final ApiHttpHeaders apiHttpHeaders = new ApiHttpHeaders(response.headers()
.toMultimap()
.entrySet()
.stream()
.flatMap(e -> e.getValue().stream().map(value -> ApiHttpHeaders.headerEntry(e.getKey(), value)))
.collect(Collectors.toList()));

final ApiHttpResponse<byte[]> apiHttpResponse = new ApiHttpResponse<>(response.code(), apiHttpHeaders,
Optional.ofNullable(response.body())
.map(Utils.wrapToCompletionException(okhttp3.ResponseBody::bytes))
.orElse(null),
response.message());
if (response.body() != null) {
response.close();
}
return apiHttpResponse;
}

private static okhttp3.Request toRequest(final ApiHttpRequest apiHttpRequest) {

okhttp3.Request.Builder httpRequestBuilder = new okhttp3.Request.Builder().url(apiHttpRequest.getUrl());

//set headers
for (Map.Entry<String, String> entry : apiHttpRequest.getHeaders().getHeaders()) {
httpRequestBuilder = httpRequestBuilder.header(entry.getKey(), entry.getValue());
}

if (apiHttpRequest.getMethod() == null) {
throw new IllegalStateException("apiHttpRequest method should be non null");
}

//default media type is JSON, if other media type is set as a header, use it
okhttp3.MediaType mediaType = JSON;
if (apiHttpRequest.getHeaders()
.getHeaders()
.stream()
.anyMatch(s -> s.getKey().equalsIgnoreCase(CONTENT_TYPE))) {
mediaType = okhttp3.MediaType
.get(Objects.requireNonNull(apiHttpRequest.getHeaders().getFirst(ApiHttpHeaders.CONTENT_TYPE)));
}

try {
final okhttp3.RequestBody body = apiHttpRequest.getBody() == null ? null
: okhttp3.RequestBody.create(apiHttpRequest.getBody(), mediaType);
httpRequestBuilder.method(apiHttpRequest.getMethod().name(), body);
}
catch (NoSuchMethodError error) {
throw new IllegalStateException(
"Request class is not compatible with this HTTP client implementation. Probably a wrong http client package is used. Please try \"commercetools-okhttp-client3\" instead");
}
return httpRequestBuilder.build();
}

private CompletableFuture<okhttp3.Response> makeRequest(final OkHttpClient client, final okhttp3.Request request) {
final okhttp3.Call call = client.newCall(request);
final OkHttpResponseFuture result = new OkHttpResponseFuture();
call.enqueue(result);
return result.future;
}

private static class OkHttpResponseFuture implements okhttp3.Callback {
public final CompletableFuture<okhttp3.Response> future = new CompletableFuture<>();

public OkHttpResponseFuture() {
}

@Override
public void onFailure(final okhttp3.Call call, final IOException e) {
future.completeExceptionally(e);
}

@Override
public void onResponse(final okhttp3.Call call, final okhttp3.Response response) throws IOException {
future.complete(response);
}
}

@Override
public void closeDelegate() throws IOException {
okHttpClient.dispatcher().executorService().shutdown();
okHttpClient.connectionPool().evictAll();
if (okHttpClient.cache() != null)
Objects.requireNonNull(okHttpClient.cache()).close();
}

public static class UnzippingInterceptor implements okhttp3.Interceptor {
@Override
@NotNull
public okhttp3.Response intercept(Chain chain) throws IOException {
okhttp3.Response response = chain.proceed(chain.request());
return unzip(response);
}

okhttp3.Response unzip(final okhttp3.Response response) {
if (!"gzip".equalsIgnoreCase(response.header("Content-Encoding"))) {
return response;
}

if (response.body() == null) {
return response;
}

okhttp3.Headers strippedHeaders = response.headers()
.newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
String contentType = response.header("Content-Type");
return response.newBuilder()
.headers(strippedHeaders)
.body(okhttp3.ResponseBody.create(Okio.buffer(new GzipSource(response.body().source())),
okhttp3.MediaType.get(contentType), -1L))
.build();
}
}
}
Loading