Skip to content

Commit 4d56b32

Browse files
authored
[Feat][SDK-594] Compress payload and update payload max size to 1mb (#371)
* feat(java): raise max payload size to 1MB * feat(java): compress outgoing payloads with gzip by default * style(java): fix import order in SyncSender * fix(java): add default implementation to compressPayload to avoid API break * feat(reactive-streams): wire gzip compression into AsyncSender matching SyncSender behaviour * fix(java): close raw stream if GZIPOutputStream constructor throws in sendJson * fix(reactive-streams): disable compression in AsyncSenderTest to avoid null getBody() * refactor(reactive-streams): use compression-intent pattern for safe third-party extensibility * fix(reactive-streams): prevent GZIPOutputStream and ByteBuf resource leaks
1 parent 08d41d7 commit 4d56b32

14 files changed

Lines changed: 231 additions & 22 deletions

File tree

rollbar-java/src/integTest/java/com/rollbar/notifier/RollbarITest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ public void ifPayloadIsTooLargeItShouldBeTruncated() {
352352
assertThat(frames, hasSize(20));
353353

354354
assertThat(PayloadTruncator.sizeInBytes(payloadJsonString),
355-
lessThanOrEqualTo(512 * 1024));
355+
lessThanOrEqualTo(1024 * 1024));
356356
}
357357

358358
protected Sender buildSender(String url, String accessToken, Proxy proxy) {

rollbar-java/src/main/java/com/rollbar/notifier/RollbarBase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
public abstract class RollbarBase<RESULT, C extends CommonConfig> {
3737

3838
private static final Logger LOGGER = LoggerFactory.getLogger(RollbarBase.class);
39-
private static final int MAX_PAYLOAD_SIZE_BYTES = 512 * 1024; // 512kb
39+
private static final int MAX_PAYLOAD_SIZE_BYTES = 1024 * 1024; // 1mb
4040

4141
protected BodyFactory bodyFactory;
4242
protected PayloadTruncator payloadTruncator;

rollbar-java/src/main/java/com/rollbar/notifier/config/CommonConfig.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ public interface CommonConfig {
212212
*/
213213
boolean truncateLargePayloads();
214214

215+
/**
216+
* <p>
217+
* If set to true (the default), payloads are gzip-compressed before sending.
218+
* Set to false to send uncompressed JSON.
219+
* </p>
220+
* @return true to compress payloads, false otherwise.
221+
*/
222+
default boolean compressPayload() {
223+
return true;
224+
}
225+
215226
int maximumTelemetryData();
216227

217228
TelemetryEventTracker telemetryEventTracker();

rollbar-java/src/main/java/com/rollbar/notifier/config/ConfigBuilder.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ public class ConfigBuilder {
8787

8888
protected boolean truncateLargePayloads;
8989

90+
protected boolean compressPayload;
91+
9092
private int maximumTelemetryData =
9193
RollbarTelemetryEventTracker.MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS;
9294

@@ -101,6 +103,7 @@ protected ConfigBuilder(String accessToken) {
101103
this.accessToken = accessToken;
102104
this.handleUncaughtErrors = true;
103105
this.enabled = true;
106+
this.compressPayload = true;
104107
this.defaultLevels = new DefaultLevels();
105108
}
106109

@@ -136,6 +139,7 @@ private ConfigBuilder(Config config) {
136139
this.appPackages = config.appPackages();
137140
this.defaultLevels = new DefaultLevels(config);
138141
this.truncateLargePayloads = config.truncateLargePayloads();
142+
this.compressPayload = config.compressPayload();
139143
this.maximumTelemetryData = config.maximumTelemetryData();
140144
this.telemetryEventTracker = config.telemetryEventTracker();
141145
}
@@ -480,6 +484,18 @@ public ConfigBuilder truncateLargePayloads(boolean truncate) {
480484
return this;
481485
}
482486

487+
/**
488+
* <p>
489+
* If set to false, payloads will not be gzip-compressed before sending. Default: true.
490+
* </p>
491+
* @param compress true to gzip-compress payloads.
492+
* @return the builder instance.
493+
*/
494+
public ConfigBuilder compressPayload(boolean compress) {
495+
this.compressPayload = compress;
496+
return this;
497+
}
498+
483499
/**
484500
* <p>
485501
* Maximum Telemetry events sent in a payload, only for the default TelemetryEventTracker, if
@@ -526,7 +542,8 @@ public Config build() {
526542
SyncSender.Builder innerSender =
527543
new SyncSender.Builder(this.endpoint)
528544
.accessToken(accessToken)
529-
.proxy(proxy);
545+
.proxy(proxy)
546+
.compressPayload(this.compressPayload);
530547
if (this.jsonSerializer != null) {
531548
innerSender.jsonSerializer(this.jsonSerializer);
532549
}
@@ -601,6 +618,8 @@ private static class ConfigImpl implements Config {
601618

602619
private final boolean truncateLargePayloads;
603620

621+
private final boolean compressPayload;
622+
604623
private final int maximumTelemetryData;
605624

606625
private final TelemetryEventTracker telemetryEventTracker;
@@ -637,6 +656,7 @@ private static class ConfigImpl implements Config {
637656
this.enabled = builder.enabled;
638657
this.defaultLevels = builder.defaultLevels;
639658
this.truncateLargePayloads = builder.truncateLargePayloads;
659+
this.compressPayload = builder.compressPayload;
640660
this.maximumTelemetryData = builder.maximumTelemetryData;
641661
this.telemetryEventTracker = builder.telemetryEventTracker;
642662
}
@@ -786,6 +806,11 @@ public boolean truncateLargePayloads() {
786806
return this.truncateLargePayloads;
787807
}
788808

809+
@Override
810+
public boolean compressPayload() {
811+
return this.compressPayload;
812+
}
813+
789814
@Override
790815
public int maximumTelemetryData() {
791816
return this.maximumTelemetryData;

rollbar-java/src/main/java/com/rollbar/notifier/sender/SyncSender.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.net.MalformedURLException;
1616
import java.net.Proxy;
1717
import java.net.URL;
18+
import java.util.zip.GZIPOutputStream;
1819

1920
/**
2021
* Synchronous implementation of the {@link Sender sender}.
@@ -33,11 +34,14 @@ public class SyncSender extends AbstractSender {
3334

3435
private final Proxy proxy;
3536

37+
private final boolean compressPayload;
38+
3639
SyncSender(Builder builder) {
3740
this.url = builder.url;
3841
this.jsonSerializer = builder.jsonSerializer;
3942
this.accessToken = builder.accessToken;
4043
this.proxy = builder.proxy != null ? builder.proxy : Proxy.NO_PROXY;
44+
this.compressPayload = builder.compressPayload;
4145
}
4246

4347
@Override
@@ -69,6 +73,9 @@ private HttpURLConnection getConnection() throws IOException {
6973
connection.setRequestProperty("Accept-Charset", UTF_8);
7074
connection.setRequestProperty("Content-Type", "application/json; charset=" + UTF_8);
7175
connection.setRequestProperty("Accept", "application/json");
76+
if (compressPayload) {
77+
connection.setRequestProperty("Content-Encoding", "gzip");
78+
}
7279
connection.setDoOutput(true);
7380
connection.setRequestMethod("POST");
7481

@@ -78,10 +85,14 @@ private HttpURLConnection getConnection() throws IOException {
7885
private void sendJson(HttpURLConnection connection, byte[] bytes) throws IOException {
7986
OutputStream out = null;
8087
try {
81-
out = connection.getOutputStream();
88+
OutputStream raw = connection.getOutputStream();
89+
try {
90+
out = compressPayload ? new GZIPOutputStream(raw) : raw;
91+
} catch (IOException e) {
92+
ObjectsUtils.close(raw);
93+
throw e;
94+
}
8295
out.write(bytes, 0, bytes.length);
83-
} catch (IOException e) {
84-
throw e;
8596
} finally {
8697
ObjectsUtils.close(out);
8798
}
@@ -131,6 +142,8 @@ public static final class Builder {
131142

132143
private Proxy proxy;
133144

145+
private boolean compressPayload = true;
146+
134147
public Builder() {
135148
this(DEFAULT_API_ENDPOINT);
136149
}
@@ -195,6 +208,16 @@ public Builder proxy(Proxy proxy) {
195208
return this;
196209
}
197210

211+
/**
212+
* Whether to gzip-compress payloads before sending. Default: true.
213+
* @param compress true to enable compression.
214+
* @return the builder instance.
215+
*/
216+
public Builder compressPayload(boolean compress) {
217+
this.compressPayload = compress;
218+
return this;
219+
}
220+
198221
/**
199222
* Builds the {@link SyncSender sync sender}.
200223
*

rollbar-java/src/test/java/com/rollbar/notifier/sender/SyncSenderTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
import com.rollbar.notifier.sender.result.Response;
1818
import com.rollbar.notifier.sender.result.Result;
1919
import java.io.ByteArrayInputStream;
20+
import java.io.ByteArrayOutputStream;
2021
import java.io.IOException;
2122
import java.io.InputStream;
2223
import java.io.OutputStream;
24+
import java.util.zip.GZIPInputStream;
2325
import java.net.HttpURLConnection;
2426
import java.net.Proxy;
2527
import java.net.URL;
@@ -76,6 +78,7 @@ public void setUp()throws Exception {
7678
sut = new SyncSender.Builder()
7779
.url(url)
7880
.jsonSerializer(serializer)
81+
.compressPayload(false)
7982
.build();
8083
sut.addListener(listener);
8184
}
@@ -198,6 +201,57 @@ public void shouldSendThePayloadUsingAProxyIfProvided() throws Exception {
198201
verify(listener).onResponse(payload, expectedResponse);
199202
}
200203

204+
@Test
205+
public void shouldSendGzipEncodedPayloadWhenCompressionEnabled() throws Exception {
206+
ByteArrayOutputStream capturedBytes = new ByteArrayOutputStream();
207+
when(url.openConnection(eq(Proxy.NO_PROXY))).thenReturn(connection);
208+
when(connection.getOutputStream()).thenReturn(capturedBytes);
209+
210+
int responseCode = 200;
211+
String responseJson = "simulated_response_json";
212+
when(connection.getResponseCode()).thenReturn(responseCode);
213+
when(connection.getInputStream())
214+
.thenReturn(new ByteArrayInputStream(responseJson.getBytes(UTF_8)));
215+
when(serializer.resultFrom(responseJson)).thenReturn(result);
216+
217+
SyncSender compressingSut = new SyncSender.Builder()
218+
.url(url)
219+
.jsonSerializer(serializer)
220+
.compressPayload(true)
221+
.build();
222+
223+
compressingSut.send(payload);
224+
225+
verify(connection).setRequestProperty("Content-Encoding", "gzip");
226+
227+
GZIPInputStream gzipIn = new GZIPInputStream(
228+
new ByteArrayInputStream(capturedBytes.toByteArray()));
229+
ByteArrayOutputStream decompressedBytes = new ByteArrayOutputStream();
230+
byte[] buf = new byte[1024];
231+
int n;
232+
while ((n = gzipIn.read(buf)) != -1) {
233+
decompressedBytes.write(buf, 0, n);
234+
}
235+
String decompressed = decompressedBytes.toString(UTF_8);
236+
assertThat(decompressed, is(PAYLOAD_JSON));
237+
}
238+
239+
@Test
240+
public void shouldNotSetContentEncodingWhenCompressionDisabled() throws Exception {
241+
int responseCode = 200;
242+
String responseJson = "simulated_response_json";
243+
when(connection.getResponseCode()).thenReturn(responseCode);
244+
when(connection.getInputStream())
245+
.thenReturn(new ByteArrayInputStream(responseJson.getBytes(UTF_8)));
246+
when(serializer.resultFrom(responseJson)).thenReturn(result);
247+
248+
sut.send(payload);
249+
250+
verify(connection, org.mockito.Mockito.never())
251+
.setRequestProperty(org.mockito.ArgumentMatchers.eq("Content-Encoding"),
252+
org.mockito.ArgumentMatchers.anyString());
253+
}
254+
201255
private void verifyHttp() throws Exception {
202256
verify(connection).setRequestProperty("Accept-Charset", UTF_8);
203257
verify(connection).setRequestProperty("Content-Type", "application/json; charset=" + UTF_8);

rollbar-reactive-streams-reactor/src/main/java/com/rollbar/reactivestreams/notifier/sender/http/ReactorAsyncHttpClient.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package com.rollbar.reactivestreams.notifier.sender.http;
22

3+
import com.rollbar.notifier.sender.SyncSender;
34
import io.netty.buffer.ByteBuf;
45
import io.netty.buffer.ByteBufAllocator;
6+
import java.io.ByteArrayOutputStream;
7+
import java.io.IOException;
58
import java.net.InetSocketAddress;
69
import java.net.Proxy;
710
import java.nio.charset.StandardCharsets;
811
import java.util.AbstractMap;
912
import java.util.Map;
13+
import java.util.zip.GZIPOutputStream;
1014

1115
import org.reactivestreams.Publisher;
1216
import reactor.core.publisher.Mono;
@@ -47,14 +51,26 @@ public class ReactorAsyncHttpClient implements AsyncHttpClient {
4751
public Publisher<AsyncHttpResponse> send(AsyncHttpRequest httpRequest) {
4852

4953
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
50-
buffer.writeCharSequence(httpRequest.getBody(), StandardCharsets.UTF_8);
54+
try {
55+
if (httpRequest.isCompressionRequested()) {
56+
buffer.writeBytes(compress(httpRequest.getBody()));
57+
} else {
58+
buffer.writeCharSequence(httpRequest.getBody(), StandardCharsets.UTF_8);
59+
}
60+
} catch (Throwable t) {
61+
buffer.release();
62+
throw t;
63+
}
5164
Mono<ByteBuf> buf = Mono.just(buffer);
5265

5366
return httpClient
5467
.headers(entries -> {
5568
for (Map.Entry<String, String> header : httpRequest.getHeaders()) {
5669
entries.add(header.getKey(), header.getValue());
5770
}
71+
if (httpRequest.isCompressionRequested()) {
72+
entries.add("Content-Encoding", "gzip");
73+
}
5874
})
5975
.post()
6076
.uri(httpRequest.getUrl())
@@ -140,6 +156,18 @@ private static ProxyProvider.Proxy getProxyType(Proxy proxy) {
140156
}
141157
}
142158

159+
private static byte[] compress(String json) {
160+
try {
161+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
162+
try (GZIPOutputStream gzip = new GZIPOutputStream(baos)) {
163+
gzip.write(json.getBytes(SyncSender.UTF_8));
164+
}
165+
return baos.toByteArray();
166+
} catch (IOException e) {
167+
throw new RuntimeException("Failed to gzip-compress payload", e);
168+
}
169+
}
170+
143171
public static final class Builder {
144172
private Proxy proxy;
145173
private ConnectionProvider connectionProvider;

0 commit comments

Comments
 (0)