Skip to content

Commit 138c9c6

Browse files
NO-SNOW: Load minicore lazily on first connection instead of driver class-load (#2670)
## Summary - Minicore native library loading is now deferred until the first actual Snowflake connection attempt (`ConnectionFactory.createConnection()`), instead of eagerly loading during `SnowflakeDriver`'s static initializer - This prevents filesystem writes (temp dir extraction) and native library loading (`dlopen` via JNA) when the driver is merely on the classpath as a transitive dependency - Mirrors the identical change in the Go driver: snowflakedb/gosnowflake#1807 ## Context When snowflake-jdbc is on the classpath, Java's `ServiceLoader` auto-discovers and loads all registered JDBC drivers when `DriverManager` is first used — even for non-Snowflake connections. The `SnowflakeDriver` static block called `DriverInitializer.initialize()` → `Minicore.initializeAsync()`, which extracts the native library to a temp directory and calls `Native.load` (JNA). This side effect fired on class-load alone. The fix removes minicore from `DriverInitializer.initialize()` and instead triggers `Minicore.initializeAsync()` lazily in `ConnectionFactory.createConnection()`, after confirming the URL has the `jdbc:snowflake://` prefix. The call remains async, idempotent (`synchronized` + null guard), and wrapped in `try/catch Throwable` so it never affects connection creation. ### Behavior note (intentional, not a regression) The first login request may more frequently report `CORE_LOAD_ERROR: "Minicore is still loading"` in `CLIENT_ENVIRONMENT` since the async load now starts concurrently with connection setup rather than having a head start from class-load time. The in-band `client_minicore_load` telemetry (sent after login) will still capture the real version. This is the same best-effort trade-off the Go driver PR accepts. ## Test plan - Added `ConnectionFactoryTest.testCreateConnectionTriggersMinicoreForSnowflakeUrl` — verifies minicore is started when `ConnectionFactory` processes a Snowflake URL - Added `ConnectionFactoryTest.testCreateConnectionDoesNotTriggerMinicoreForNonSnowflakeUrl` — verifies non-Snowflake URLs don't trigger minicore - Updated `DriverInitializerTest.testInitializeDoesNotTriggerMinicore` — verifies `DriverInitializer.initialize()` no longer starts minicore - Added `@AfterEach` cleanup to `DriverInitializerTest` for test isolation - Existing `MinicoreTest`, `MinicoreTelemetryWiremockIT`, and `MinicoreTelemetryTest` remain passing (they test minicore APIs directly) - `mvn com.spotify.fmt:fmt-maven-plugin:format` — 0 reformatted - `mvn clean validate -P check-style` — 0 checkstyle violations Made with [Cursor](https://cursor.com)
1 parent ea84cef commit 138c9c6

6 files changed

Lines changed: 93 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Changelog
44
- v4.3.2-SNAPSHOT
55
- Bumped jackson-databind to 2.18.8 from 2.18.7 (snowflakedb/snowflake-jdbc#2669).
6+
- Fixed snowflake-jdbc writing a `snowflake-minicore-*` temp directory and loading the native library at driver class-load time even when the driver was never used (e.g. when present on the classpath only as a transitive dependency). Minicore now loads lazily when the first Snowflake connection is created (`ConnectionFactory.createConnection`) instead of during `DriverInitializer.initialize()` (snowflakedb/snowflake-jdbc#2670).
67

78
- v4.3.1
89
- Fixed GCS-backed internal stage PUT failing with opaque `invalid_gcs_credentials` in SPCS pods on GCP: the GCS SDK's Application Default Credentials (ADC) probe was reaching out to `metadata.google.internal` which is unreachable inside SPCS; explicit credentials are now always set when a `GCS_ACCESS_TOKEN` is present, suppressing the ADC probe entirely. Also fixed `GCSAccessStrategyAwsSdk` rejecting custom GCS endpoints that lack an `https://` scheme prefix (e.g. bare `storage.me-central2.rep.googleapis.com`), mirroring the existing handling in `GCSDefaultAccessStrategy`. The catch-all in `setupGCSClient` now chains and logs the original exception instead of swallowing it (snowflakedb/snowflake-jdbc#2664).

src/main/java/net/snowflake/client/internal/core/minicore/Minicore.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,16 @@ private Minicore(MinicoreLoadResult loadResult, MinicoreLibrary library) {
2424
this.library = library;
2525
}
2626

27-
public static synchronized void initializeAsync() {
27+
public static void initializeAsync() {
2828
if (INITIALIZATION_FUTURE != null) {
29-
return; // Already started
29+
return; // Already started — volatile read only, no lock
30+
}
31+
initializeAsyncSlow();
32+
}
33+
34+
private static synchronized void initializeAsyncSlow() {
35+
if (INITIALIZATION_FUTURE != null) {
36+
return; // Re-check under lock
3037
}
3138

3239
// Check if minicore is disabled via environment variable
@@ -92,7 +99,7 @@ public MinicoreLoadResult getLoadResult() {
9299
return loadResult;
93100
}
94101

95-
public static synchronized boolean hasInitializationStarted() {
102+
public static boolean hasInitializationStarted() {
96103
return INITIALIZATION_FUTURE != null;
97104
}
98105

src/main/java/net/snowflake/client/internal/driver/ConnectionFactory.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import net.snowflake.client.api.exception.SnowflakeSQLException;
88
import net.snowflake.client.internal.api.implementation.connection.SnowflakeConnectionImpl;
99
import net.snowflake.client.internal.config.ConnectionParameters;
10+
import net.snowflake.client.internal.core.minicore.Minicore;
1011
import net.snowflake.client.internal.jdbc.SnowflakeConnectString;
1112
import net.snowflake.client.internal.log.SFLogger;
1213
import net.snowflake.client.internal.log.SFLoggerFactory;
@@ -52,6 +53,17 @@ public static Connection createConnection(String url, Properties info) throws SQ
5253
return null;
5354
}
5455

56+
// Begin loading minicore asynchronously as soon as we confirm this is a
57+
// Snowflake connection attempt. This gives the native library a head start
58+
// to be warm by first login, without loading at driver class-load time when
59+
// the driver is merely on the classpath as a transitive dependency
60+
// (SNOW-3561155 / gosnowflake#1807).
61+
try {
62+
Minicore.initializeAsync();
63+
} catch (Throwable t) {
64+
logger.trace("Failed to start minicore initialization: {}", t.getMessage());
65+
}
66+
5567
// Parse and validate the connection string
5668
SnowflakeConnectString connectString =
5769
SnowflakeConnectString.parse(params.getUrl(), params.getParams());

src/main/java/net/snowflake/client/internal/driver/DriverInitializer.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import java.lang.reflect.Field;
44
import java.lang.reflect.Method;
55
import net.snowflake.client.internal.core.SecurityUtil;
6-
import net.snowflake.client.internal.core.minicore.Minicore;
76
import net.snowflake.client.internal.jdbc.SnowflakeUtil;
87
import net.snowflake.client.internal.jdbc.telemetryOOB.TelemetryService;
98
import net.snowflake.client.internal.log.SFLogger;
@@ -52,7 +51,6 @@ public static synchronized void initialize() {
5251
initializeArrowSupport();
5352
initializeSecurityProvider();
5453
initializeTelemetry();
55-
initializeMinicore();
5654

5755
initialized = true;
5856
logger.debug("Snowflake JDBC Driver initialization complete");
@@ -159,21 +157,6 @@ private static void initializeTelemetry() {
159157
}
160158
}
161159

162-
/**
163-
* Start asynchronous minicore native library loading.
164-
*
165-
* <p>Minicore is loaded in the background so it can overlap with connection setup. The loading
166-
* result is reported via telemetry during session establishment.
167-
*/
168-
private static void initializeMinicore() {
169-
try {
170-
Minicore.initializeAsync();
171-
logger.debug("Minicore async initialization started");
172-
} catch (Throwable t) {
173-
logger.trace("Failed to start minicore initialization", t);
174-
}
175-
}
176-
177160
// Public accessors for Arrow status
178161

179162
/**
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package net.snowflake.client.internal.driver;
2+
3+
import static org.junit.jupiter.api.Assertions.assertFalse;
4+
import static org.junit.jupiter.api.Assertions.assertNull;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.util.Properties;
8+
import net.snowflake.client.category.TestTags;
9+
import net.snowflake.client.internal.core.minicore.Minicore;
10+
import org.junit.jupiter.api.AfterEach;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.Tag;
13+
import org.junit.jupiter.api.Test;
14+
15+
@Tag(TestTags.CORE)
16+
public class ConnectionFactoryTest {
17+
18+
@BeforeEach
19+
public void setUp() {
20+
Minicore.resetForTesting();
21+
}
22+
23+
@AfterEach
24+
public void tearDown() {
25+
Minicore.resetForTesting();
26+
}
27+
28+
@Test
29+
public void testCreateConnectionTriggersMinicoreForSnowflakeUrl() {
30+
assertFalse(
31+
Minicore.hasInitializationStarted(), "Minicore should not be started before connection");
32+
33+
try {
34+
ConnectionFactory.createConnection(
35+
"jdbc:snowflake://fake.account.snowflakecomputing.com", new Properties());
36+
} catch (Exception e) {
37+
// Expected — the fake host will fail during connection setup.
38+
// We only care that minicore was triggered before the failure.
39+
}
40+
41+
assertTrue(
42+
Minicore.hasInitializationStarted(),
43+
"Minicore should be started when ConnectionFactory processes a Snowflake URL");
44+
}
45+
46+
@Test
47+
public void testCreateConnectionDoesNotTriggerMinicoreForNonSnowflakeUrl() throws Exception {
48+
assertFalse(
49+
Minicore.hasInitializationStarted(), "Minicore should not be started before connection");
50+
51+
assertNull(
52+
ConnectionFactory.createConnection("jdbc:mysql://localhost:3306/db", new Properties()),
53+
"Non-Snowflake URL should return null per JDBC spec");
54+
55+
assertFalse(
56+
Minicore.hasInitializationStarted(),
57+
"Minicore should not be started for non-Snowflake URLs");
58+
}
59+
}
Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
package net.snowflake.client.internal.driver;
22

3+
import static org.junit.jupiter.api.Assertions.assertFalse;
34
import static org.junit.jupiter.api.Assertions.assertTrue;
45

56
import net.snowflake.client.category.TestTags;
67
import net.snowflake.client.internal.core.minicore.Minicore;
8+
import org.junit.jupiter.api.AfterEach;
79
import org.junit.jupiter.api.Tag;
810
import org.junit.jupiter.api.Test;
911

1012
@Tag(TestTags.CORE)
1113
public class DriverInitializerTest {
1214

15+
@AfterEach
16+
public void tearDown() {
17+
DriverInitializer.resetForTesting();
18+
Minicore.resetForTesting();
19+
}
20+
1321
@Test
14-
public void testInitializeTriggersMinicore() {
22+
public void testInitializeDoesNotTriggerMinicore() {
1523
DriverInitializer.resetForTesting();
1624
Minicore.resetForTesting();
1725

1826
DriverInitializer.initialize();
1927

2028
assertTrue(DriverInitializer.isInitialized(), "DriverInitializer should be initialized");
21-
assertTrue(
29+
assertFalse(
2230
Minicore.hasInitializationStarted(),
23-
"Minicore async initialization should have been started by DriverInitializer.initialize()");
31+
"Minicore should not be started by DriverInitializer — it loads lazily on first connection");
2432
}
2533
}

0 commit comments

Comments
 (0)