Skip to content

Commit e11d03e

Browse files
authored
fix: Truncate query in exceptions and add test coverage for protocol limits (#15)
With this change we are checking limits around: - headers - request size - parameter sizes The driver already passed all limits but the tests uncoverd a significant inefficiency around exception creation. With multi megabyte queries this was causing multi second hangs. Thus we now truncate the query in the exception message. If the query is truncated it'll have a `<truncated>` suffix
1 parent a1e5d3c commit e11d03e

File tree

7 files changed

+174
-7
lines changed

7 files changed

+174
-7
lines changed

src/main/java/com/salesforce/datacloud/jdbc/core/HyperGrpcClientExecutor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
@Slf4j
5050
@Builder(toBuilder = true)
5151
public class HyperGrpcClientExecutor implements AutoCloseable {
52-
private static final int GRPC_INBOUND_MESSAGE_MAX_SIZE = 128 * 1024 * 1024;
52+
private static final int GRPC_INBOUND_MESSAGE_MAX_SIZE = 64 * 1024 * 1024;
5353

5454
@NonNull private final ManagedChannel channel;
5555

src/main/java/com/salesforce/datacloud/jdbc/core/StreamingResultSet.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public static StreamingResultSet of(String sql, QueryStatusListener listener) {
6969

7070
return result;
7171
} catch (Exception ex) {
72-
throw QueryExceptionHandler.createException(QUERY_FAILURE + sql, ex);
72+
throw QueryExceptionHandler.createQueryException(sql, ex);
7373
}
7474
}
7575

@@ -87,6 +87,4 @@ public String getStatus() {
8787
public boolean isReady() {
8888
return listener.isReady();
8989
}
90-
91-
private static final String QUERY_FAILURE = "Failed to execute query: ";
9290
}

src/main/java/com/salesforce/datacloud/jdbc/core/listener/AdaptiveQueryStatusListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public static AdaptiveQueryStatusListener of(String query, HyperGrpcClientExecut
7878
new AdaptiveQueryStatusPoller(queryId, client),
7979
new AsyncQueryStatusPoller(queryId, client));
8080
} catch (StatusRuntimeException ex) {
81-
throw QueryExceptionHandler.createException("Failed to execute query: " + query, ex);
81+
throw QueryExceptionHandler.createQueryException(query, ex);
8282
}
8383
}
8484

src/main/java/com/salesforce/datacloud/jdbc/core/listener/AsyncQueryStatusListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public static AsyncQueryStatusListener of(String query, HyperGrpcClientExecutor
6161
.client(client)
6262
.build();
6363
} catch (StatusRuntimeException ex) {
64-
throw QueryExceptionHandler.createException("Failed to execute query: " + query, ex);
64+
throw QueryExceptionHandler.createQueryException(query, ex);
6565
}
6666
}
6767

src/main/java/com/salesforce/datacloud/jdbc/core/listener/SyncQueryStatusListener.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public static SyncQueryStatusListener of(String query, HyperGrpcClientExecutor c
6262
.initial(result)
6363
.build();
6464
} catch (StatusRuntimeException ex) {
65-
throw QueryExceptionHandler.createException("Failed to execute query: " + query, ex);
65+
throw QueryExceptionHandler.createQueryException(query, ex);
6666
}
6767
}
6868

src/main/java/com/salesforce/datacloud/jdbc/exception/QueryExceptionHandler.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@
2828
@Slf4j
2929
@UtilityClass
3030
public class QueryExceptionHandler {
31+
// We introduce a limit to avoid truncating important details from the log due to large queries.
32+
// When testing with 60 MB queries the exception formatting also took multi second hangs.
33+
private static final int MAX_QUERY_LENGTH_IN_EXCEPTION = 16 * 1024;
34+
35+
public static DataCloudJDBCException createQueryException(String query, Exception e) {
36+
String exceptionQuery = query.length() > MAX_QUERY_LENGTH_IN_EXCEPTION
37+
? query.substring(0, MAX_QUERY_LENGTH_IN_EXCEPTION) + "<truncated>"
38+
: query;
39+
return QueryExceptionHandler.createException("Failed to execute query: " + exceptionQuery, e);
40+
}
3141

3242
public static DataCloudJDBCException createException(String message, Exception e) {
3343
if (e instanceof StatusRuntimeException) {
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright (c) 2024, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.salesforce.datacloud.jdbc.core;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
20+
21+
import com.google.common.collect.Maps;
22+
import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
23+
import com.salesforce.datacloud.jdbc.hyper.HyperTestBase;
24+
import lombok.SneakyThrows;
25+
import lombok.val;
26+
import org.apache.commons.lang3.StringUtils;
27+
import org.junit.jupiter.api.Test;
28+
29+
public class JDBCLimitsTest extends HyperTestBase {
30+
@Test
31+
@SneakyThrows
32+
public void testLargeQuery() {
33+
String query = "SELECT 'a', /*" + StringUtils.repeat('x', 62 * 1024 * 1024) + "*/ 'b'";
34+
// Verify that the full SQL string is submitted by checking that the value before and after the large
35+
// comment are returned
36+
assertWithStatement(statement -> {
37+
val result = statement.executeQuery(query);
38+
result.next();
39+
assertThat(result.getString(1)).isEqualTo("a");
40+
assertThat(result.getString(2)).isEqualTo("b");
41+
});
42+
}
43+
44+
@Test
45+
@SneakyThrows
46+
public void testTooLargeQuery() {
47+
String query = "SELECT 'a', /*" + StringUtils.repeat('x', 65 * 1024 * 1024) + "*/ 'b'";
48+
assertWithStatement(statement -> {
49+
assertThatExceptionOfType(DataCloudJDBCException.class)
50+
.isThrownBy(() -> {
51+
statement.executeQuery(query);
52+
})
53+
// Also verify that we don't explode exception sizes by keeping the full query
54+
.withMessageEndingWith("<truncated>")
55+
.satisfies(t -> assertThat(t.getMessage()).hasSizeLessThan(16500));
56+
});
57+
}
58+
59+
@Test
60+
@SneakyThrows
61+
public void testLargeRowResponse() {
62+
// 31 MB is the expected max row size configured in Hyper
63+
String value = StringUtils.repeat('x', 31 * 1024 * 1024);
64+
String query = "SELECT rpad('', 31*1024*1024, 'x')";
65+
// Verify that large responses are supported
66+
assertWithStatement(statement -> {
67+
val result = statement.executeQuery(query);
68+
result.next();
69+
assertThat(result.getString(1)).isEqualTo(value);
70+
});
71+
}
72+
73+
@Test
74+
@SneakyThrows
75+
public void testTooLargeRowResponse() {
76+
// 31 MB is the expected max row size configured in Hyper, thus 33 MB should be too large
77+
String query = "SELECT rpad('', 33*1024*1024, 'x')";
78+
assertWithStatement(statement -> {
79+
assertThatExceptionOfType(DataCloudJDBCException.class)
80+
.isThrownBy(() -> {
81+
statement.executeQuery(query);
82+
})
83+
.withMessageContaining("tuple size limit exceeded");
84+
});
85+
}
86+
87+
@Test
88+
@SneakyThrows
89+
public void testLargeParameterRoundtrip() {
90+
// 31 MB is the expected max row size configured in Hyper
91+
String value = StringUtils.repeat('x', 31 * 1024 * 1024);
92+
// Verify that large responses are supported
93+
assertWithConnection(connection -> {
94+
val stmt = connection.prepareStatement("SELECT ?");
95+
stmt.setString(1, value);
96+
val result = stmt.executeQuery();
97+
result.next();
98+
assertThat(result.getString(1)).isEqualTo(value);
99+
});
100+
}
101+
102+
@Test
103+
@SneakyThrows
104+
public void testLargeParameter() {
105+
// We can send requests of up to 64MB so this parameter should still be accepted
106+
String value = StringUtils.repeat('x', 63 * 1024 * 1024);
107+
// Verify that large responses are supported
108+
assertWithConnection(connection -> {
109+
val stmt = connection.prepareStatement("SELECT length(?)");
110+
stmt.setString(1, value);
111+
val result = stmt.executeQuery();
112+
result.next();
113+
assertThat(result.getInt(1)).isEqualTo(value.length());
114+
});
115+
}
116+
117+
@Test
118+
@SneakyThrows
119+
public void testTooLargeParameter() {
120+
// We can send requests of up to 64MB so this parameter should fail
121+
String value = StringUtils.repeat('x', 64 * 1024 * 1024);
122+
assertWithConnection(connection -> {
123+
assertThatExceptionOfType(DataCloudJDBCException.class).isThrownBy(() -> {
124+
val stmt = connection.prepareStatement("SELECT length(?)");
125+
stmt.setString(1, value);
126+
stmt.executeQuery();
127+
});
128+
});
129+
}
130+
131+
@Test
132+
@SneakyThrows
133+
public void testLargeHeaders() {
134+
// We expect that under 1 MB total header size should be fine, we use workload as it'll get injected into the
135+
// header
136+
val settings = Maps.immutableEntry("workload", StringUtils.repeat('x', 1000 * 1024));
137+
assertWithStatement(
138+
statement -> {
139+
val result = statement.executeQuery("SELECT 'A'");
140+
result.next();
141+
assertThat(result.getString(1)).isEqualTo("A");
142+
},
143+
settings);
144+
}
145+
146+
@Test
147+
@SneakyThrows
148+
public void testTooLargeHeaders() {
149+
// We expect that due to 1 MB total header size limit, setting such a large workload should fail
150+
val settings = Maps.immutableEntry("workload", StringUtils.repeat('x', 1024 * 1024));
151+
assertWithStatement(
152+
statement -> {
153+
assertThatExceptionOfType(DataCloudJDBCException.class).isThrownBy(() -> {
154+
statement.executeQuery("SELECT 'A'");
155+
});
156+
},
157+
settings);
158+
}
159+
}

0 commit comments

Comments
 (0)