Skip to content

Commit e899319

Browse files
SNOW-1003775: Fix parsing large string values with jackson databind > 2.15 (#1613)
1 parent e0f4fa2 commit e899319

File tree

8 files changed

+183
-35
lines changed

8 files changed

+183
-35
lines changed

src/main/java/net/snowflake/client/core/HttpUtil.java

+6-28
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,14 @@ public class HttpUtil {
109109
@SnowflakeJdbcInternalApi
110110
public static Duration getConnectionTimeout() {
111111
return Duration.ofMillis(
112-
convertSystemPropertyToIntValue(
112+
SystemUtil.convertSystemPropertyToIntValue(
113113
JDBC_CONNECTION_TIMEOUT_IN_MS_PROPERTY, DEFAULT_HTTP_CLIENT_CONNECTION_TIMEOUT_IN_MS));
114114
}
115115

116116
@SnowflakeJdbcInternalApi
117117
public static Duration getSocketTimeout() {
118118
return Duration.ofMillis(
119-
convertSystemPropertyToIntValue(
119+
SystemUtil.convertSystemPropertyToIntValue(
120120
JDBC_SOCKET_TIMEOUT_IN_MS_PROPERTY, DEFAULT_HTTP_CLIENT_SOCKET_TIMEOUT_IN_MS));
121121
}
122122

@@ -305,7 +305,7 @@ public static CloseableHttpClient buildHttpClient(
305305
@Nullable HttpClientSettingsKey key, File ocspCacheFile, boolean downloadUnCompressed) {
306306
// set timeout so that we don't wait forever.
307307
// Setup the default configuration for all requests on this client
308-
int timeToLive = convertSystemPropertyToIntValue(JDBC_TTL, DEFAULT_TTL);
308+
int timeToLive = SystemUtil.convertSystemPropertyToIntValue(JDBC_TTL, DEFAULT_TTL);
309309
logger.debug("time to live in connection pooling manager: {}", timeToLive);
310310
long connectTimeout = getConnectionTimeout().toMillis();
311311
long socketTimeout = getSocketTimeout().toMillis();
@@ -373,9 +373,10 @@ public static CloseableHttpClient buildHttpClient(
373373
new PoolingHttpClientConnectionManager(
374374
registry, null, null, null, timeToLive, TimeUnit.SECONDS);
375375
int maxConnections =
376-
convertSystemPropertyToIntValue(JDBC_MAX_CONNECTIONS_PROPERTY, DEFAULT_MAX_CONNECTIONS);
376+
SystemUtil.convertSystemPropertyToIntValue(
377+
JDBC_MAX_CONNECTIONS_PROPERTY, DEFAULT_MAX_CONNECTIONS);
377378
int maxConnectionsPerRoute =
378-
convertSystemPropertyToIntValue(
379+
SystemUtil.convertSystemPropertyToIntValue(
379380
JDBC_MAX_CONNECTIONS_PER_ROUTE_PROPERTY, DEFAULT_MAX_CONNECTIONS_PER_ROUTE);
380381
logger.debug(
381382
"Max connections total in connection pooling manager: {}; max connections per route: {}",
@@ -901,29 +902,6 @@ public Socket createSocket(HttpContext ctx) throws IOException {
901902
}
902903
}
903904

904-
/**
905-
* Helper function to convert system properties to integers
906-
*
907-
* @param systemProperty name of the system property
908-
* @param defaultValue default value used
909-
* @return the value of the system property, else the default value
910-
*/
911-
static int convertSystemPropertyToIntValue(String systemProperty, int defaultValue) {
912-
String systemPropertyValue = systemGetProperty(systemProperty);
913-
int returnVal = defaultValue;
914-
if (systemPropertyValue != null) {
915-
try {
916-
returnVal = Integer.parseInt(systemPropertyValue);
917-
} catch (NumberFormatException ex) {
918-
logger.info(
919-
"Failed to parse the system parameter {} with value {}",
920-
systemProperty,
921-
systemPropertyValue);
922-
}
923-
}
924-
return returnVal;
925-
}
926-
927905
/**
928906
* Helper function to attach additional headers to a request if present. This takes a (nullable)
929907
* map of headers in <name,value> format and adds them to the incoming request using addHeader.
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
11
package net.snowflake.client.core;
22

3+
import com.fasterxml.jackson.core.StreamReadConstraints;
34
import com.fasterxml.jackson.databind.MapperFeature;
45
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import net.snowflake.client.log.SFLogger;
7+
import net.snowflake.client.log.SFLoggerFactory;
58

69
/**
710
* Factor method used to create ObjectMapper instance. All object mapper in JDBC should be created
811
* by this method.
912
*/
1013
public class ObjectMapperFactory {
14+
@SnowflakeJdbcInternalApi
15+
// Snowflake allows up to 16M string size and returns base64 encoded value that makes it up to 23M
16+
public static final int DEFAULT_MAX_JSON_STRING_LEN = 23_000_000;
17+
18+
public static final String MAX_JSON_STRING_LENGTH_JVM =
19+
"net.snowflake.jdbc.objectMapper.maxJsonStringLength";
20+
21+
private static final SFLogger logger = SFLoggerFactory.getLogger(ObjectMapperFactory.class);
22+
1123
public static ObjectMapper getObjectMapper() {
1224
ObjectMapper mapper = new ObjectMapper();
1325
mapper.configure(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS, false);
1426
mapper.configure(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS, false);
1527

28+
// override the maxStringLength value in ObjectMapper
29+
int maxJsonStringLength =
30+
SystemUtil.convertSystemPropertyToIntValue(
31+
MAX_JSON_STRING_LENGTH_JVM, DEFAULT_MAX_JSON_STRING_LEN);
32+
mapper
33+
.getFactory()
34+
.setStreamReadConstraints(
35+
StreamReadConstraints.builder().maxStringLength(maxJsonStringLength).build());
1636
return mapper;
1737
}
1838
}

src/main/java/net/snowflake/client/core/SFSession.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ public class SFSession extends SFBaseSession {
4747
// Need to be removed when a better way to organize session parameter is introduced.
4848
private static final String CLIENT_STORE_TEMPORARY_CREDENTIAL =
4949
"CLIENT_STORE_TEMPORARY_CREDENTIAL";
50-
private static final ObjectMapper mapper = ObjectMapperFactory.getObjectMapper();
5150
private static final int MAX_SESSION_PARAMETERS = 1000;
5251
// this constant was public - let's not change it
5352
public static final int DEFAULT_HTTP_CLIENT_SOCKET_TIMEOUT =
@@ -931,7 +930,7 @@ protected void heartbeat() throws SFException, SQLException {
931930

932931
logger.debug("connection heartbeat response: {}", theResponse);
933932

934-
rootNode = mapper.readTree(theResponse);
933+
rootNode = OBJECT_MAPPER.readTree(theResponse);
935934

936935
// check the response to see if it is session expiration response
937936
if (rootNode != null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved.
3+
*/
4+
5+
package net.snowflake.client.core;
6+
7+
import static net.snowflake.client.jdbc.SnowflakeUtil.systemGetProperty;
8+
9+
import net.snowflake.client.log.SFLogger;
10+
import net.snowflake.client.log.SFLoggerFactory;
11+
12+
class SystemUtil {
13+
private static final SFLogger logger = SFLoggerFactory.getLogger(SystemUtil.class);
14+
15+
/**
16+
* Helper function to convert system properties to integers
17+
*
18+
* @param systemProperty name of the system property
19+
* @param defaultValue default value used
20+
* @return the value of the system property, else the default value
21+
*/
22+
static int convertSystemPropertyToIntValue(String systemProperty, int defaultValue) {
23+
String systemPropertyValue = systemGetProperty(systemProperty);
24+
int returnVal = defaultValue;
25+
if (systemPropertyValue != null) {
26+
try {
27+
returnVal = Integer.parseInt(systemPropertyValue);
28+
} catch (NumberFormatException ex) {
29+
logger.info(
30+
"Failed to parse the system parameter {} with value {}",
31+
systemProperty,
32+
systemPropertyValue);
33+
}
34+
}
35+
return returnVal;
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright (c) 2012-2024 Snowflake Computing Inc. All rights reserved.
3+
*/
4+
5+
package net.snowflake.client.core;
6+
7+
import static org.junit.Assert.assertEquals;
8+
9+
import com.fasterxml.jackson.databind.JsonNode;
10+
import com.fasterxml.jackson.databind.ObjectMapper;
11+
import java.nio.charset.StandardCharsets;
12+
import java.sql.SQLException;
13+
import java.util.ArrayList;
14+
import java.util.Base64;
15+
import java.util.Collection;
16+
import java.util.List;
17+
import net.snowflake.client.jdbc.SnowflakeUtil;
18+
import org.junit.After;
19+
import org.junit.Assert;
20+
import org.junit.Test;
21+
import org.junit.runner.RunWith;
22+
import org.junit.runners.Parameterized;
23+
24+
@RunWith(Parameterized.class)
25+
public class ObjectMapperTest {
26+
private static final int jacksonDefaultMaxStringLength = 20_000_000;
27+
28+
@Parameterized.Parameters(name = "lobSizeInMB={0}, maxJsonStringLength={1}")
29+
public static Collection<Object[]> data() {
30+
int[] lobSizeInMB = new int[] {16, 16, 32, 64, 128};
31+
// maxJsonStringLength to be set for the corresponding LOB size
32+
int[] maxJsonStringLengths =
33+
new int[] {jacksonDefaultMaxStringLength, 23_000_000, 45_000_000, 90_000_000, 180_000_000};
34+
List<Object[]> ret = new ArrayList<>();
35+
for (int i = 0; i < lobSizeInMB.length; i++) {
36+
ret.add(new Object[] {lobSizeInMB[i], maxJsonStringLengths[i]});
37+
}
38+
return ret;
39+
}
40+
41+
private final int lobSizeInBytes;
42+
private final int maxJsonStringLength;
43+
44+
@After
45+
public void clearProperty() {
46+
System.clearProperty(ObjectMapperFactory.MAX_JSON_STRING_LENGTH_JVM);
47+
}
48+
49+
public ObjectMapperTest(int lobSizeInMB, int maxJsonStringLength) {
50+
// convert LOB size from MB to bytes
51+
this.lobSizeInBytes = lobSizeInMB * 1024 * 1024;
52+
this.maxJsonStringLength = maxJsonStringLength;
53+
System.setProperty(
54+
ObjectMapperFactory.MAX_JSON_STRING_LENGTH_JVM, Integer.toString(maxJsonStringLength));
55+
}
56+
57+
@Test
58+
public void testInvalidMaxJsonStringLength() throws SQLException {
59+
System.setProperty(ObjectMapperFactory.MAX_JSON_STRING_LENGTH_JVM, "abc");
60+
// calling getObjectMapper() should log the exception but not throw
61+
// default maxJsonStringLength value will be used
62+
ObjectMapper mapper = ObjectMapperFactory.getObjectMapper();
63+
int stringLengthInMapper = mapper.getFactory().streamReadConstraints().getMaxStringLength();
64+
Assert.assertEquals(ObjectMapperFactory.DEFAULT_MAX_JSON_STRING_LEN, stringLengthInMapper);
65+
}
66+
67+
@Test
68+
public void testObjectMapperWithLargeJsonString() {
69+
ObjectMapper mapper = ObjectMapperFactory.getObjectMapper();
70+
try {
71+
JsonNode jsonNode = mapper.readTree(generateBase64EncodedJsonString(lobSizeInBytes));
72+
Assert.assertNotNull(jsonNode);
73+
} catch (Exception e) {
74+
// exception is expected when jackson's default maxStringLength value is used while retrieving
75+
// 16M string data
76+
assertEquals(jacksonDefaultMaxStringLength, maxJsonStringLength);
77+
}
78+
}
79+
80+
private String generateBase64EncodedJsonString(int numChar) {
81+
StringBuilder jsonStr = new StringBuilder();
82+
String largeStr = SnowflakeUtil.randomAlphaNumeric(numChar);
83+
84+
// encode the string and put it into a JSON formatted string
85+
jsonStr.append("[\"").append(encodeStringToBase64(largeStr)).append("\"]");
86+
return jsonStr.toString();
87+
}
88+
89+
private String encodeStringToBase64(String stringToBeEncoded) {
90+
return Base64.getEncoder().encodeToString(stringToBeEncoded.getBytes(StandardCharsets.UTF_8));
91+
}
92+
}

src/test/java/net/snowflake/client/core/SessionUtilLatestIT.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,12 @@ private Map<SFSessionProperty, Object> initConnectionPropertiesMap() {
112112
public void testConvertSystemPropertyToIntValue() {
113113
// SNOW-760642 - Test that new default for net.snowflake.jdbc.ttl is 60 seconds.
114114
assertEquals(
115-
60, HttpUtil.convertSystemPropertyToIntValue(HttpUtil.JDBC_TTL, HttpUtil.DEFAULT_TTL));
115+
60, SystemUtil.convertSystemPropertyToIntValue(HttpUtil.JDBC_TTL, HttpUtil.DEFAULT_TTL));
116116

117117
// Test that TTL can be disabled
118118
System.setProperty(HttpUtil.JDBC_TTL, "-1");
119119
assertEquals(
120-
-1, HttpUtil.convertSystemPropertyToIntValue(HttpUtil.JDBC_TTL, HttpUtil.DEFAULT_TTL));
120+
-1, SystemUtil.convertSystemPropertyToIntValue(HttpUtil.JDBC_TTL, HttpUtil.DEFAULT_TTL));
121121
}
122122

123123
/**

src/test/java/net/snowflake/client/core/SessionUtilTest.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,19 @@ public void testConvertSystemPropertyToIntValue() {
7373
System.setProperty("net.snowflake.jdbc.max_connections", "500");
7474
assertEquals(
7575
500,
76-
HttpUtil.convertSystemPropertyToIntValue(
76+
SystemUtil.convertSystemPropertyToIntValue(
7777
HttpUtil.JDBC_MAX_CONNECTIONS_PROPERTY, HttpUtil.DEFAULT_MAX_CONNECTIONS));
7878
// Test that entering a non-int sets the value to the default
7979
System.setProperty("net.snowflake.jdbc.max_connections", "notAnInteger");
8080
assertEquals(
8181
HttpUtil.DEFAULT_MAX_CONNECTIONS,
82-
HttpUtil.convertSystemPropertyToIntValue(
82+
SystemUtil.convertSystemPropertyToIntValue(
8383
HttpUtil.JDBC_MAX_CONNECTIONS_PROPERTY, HttpUtil.DEFAULT_MAX_CONNECTIONS));
8484
// Test another system property
8585
System.setProperty("net.snowflake.jdbc.max_connections_per_route", "30");
8686
assertEquals(
8787
30,
88-
HttpUtil.convertSystemPropertyToIntValue(
88+
SystemUtil.convertSystemPropertyToIntValue(
8989
HttpUtil.JDBC_MAX_CONNECTIONS_PER_ROUTE_PROPERTY,
9090
HttpUtil.DEFAULT_MAX_CONNECTIONS_PER_ROUTE));
9191
}

src/test/java/net/snowflake/client/jdbc/ResultSetLatestIT.java

+22
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import net.snowflake.client.RunningOnGithubAction;
2222
import net.snowflake.client.TestUtil;
2323
import net.snowflake.client.category.TestCategoryResultSet;
24+
import net.snowflake.client.core.ObjectMapperFactory;
2425
import net.snowflake.client.core.SFBaseSession;
2526
import net.snowflake.client.core.SessionUtil;
2627
import net.snowflake.client.jdbc.telemetry.*;
@@ -924,4 +925,25 @@ public void testGranularTimeFunctionsInUTC() throws SQLException {
924925
connection.close();
925926
}
926927
}
928+
929+
/** Added in > 3.14.5 */
930+
@Test
931+
public void testLargeStringRetrieval() throws SQLException {
932+
String tableName = "maxJsonStringLength_table";
933+
int colLength = 16777216;
934+
try (Connection con = getConnection();
935+
Statement statement = con.createStatement()) {
936+
statement.execute("create or replace table " + tableName + " (c1 string(" + colLength + "))");
937+
statement.execute(
938+
"insert into " + tableName + " select randstr(" + colLength + ", random())");
939+
assertNull(System.getProperty(ObjectMapperFactory.MAX_JSON_STRING_LENGTH_JVM));
940+
try (ResultSet rs = statement.executeQuery("select * from " + tableName)) {
941+
assertTrue(rs.next());
942+
assertEquals(colLength, rs.getString(1).length());
943+
assertFalse(rs.next());
944+
}
945+
} catch (Exception e) {
946+
fail("executeQuery should not fail");
947+
}
948+
}
927949
}

0 commit comments

Comments
 (0)