Skip to content

Commit a1f4bca

Browse files
NO-SNOW: Fix chunk download/parse timing split in metrics log (#2640)
1 parent 222a005 commit a1f4bca

2 files changed

Lines changed: 67 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- Fixed OAuth token requests sending `scope=session:role:null` when no scope is configured (or scope is empty/blank); the `scope` parameter is now omitted entirely in those cases (snowflakedb/snowflake-jdbc#2646).
2525
- Fixed Okta native SSO federated login sending malformed JSON to `/api/v1/authn` (HTTP 400 from Okta) when the username or password contained JSON-special characters such as double quotes or backslashes; the request body is now serialized with Jackson instead of string concatenation.
2626
- Added one in-band telemetry record per successful login describing which connection-identifier fields the user supplied (`account_provided`, `account_with_region`, `account_org_provided`, `region_provided`, `host_provided`). No hostname or account value is included. This is gated by the existing server-side `CLIENT_TELEMETRY_ENABLED` parameter and can additionally be disabled locally by setting `SF_TELEMETRY_DISABLE_CONNECTION_SHAPE=true`. The telemetry collection is time-boxed and will be removed in a future release.
27+
- Fixed `SnowflakeChunkDownloader` per-chunk metrics log misattributing the response body transfer to `parseTime`: `getResultStreamProvider().getInputStream()` returns once headers come back and streams the body lazily during the parser's `read()` calls, so the old code billed only HTTP/TLS setup to `downloadTime` and the entire body read+parse to `parseTime`. When the metrics logger is at `FINE`/debug level, the InputStream is now wrapped in a `TimingInputStream` that accumulates time blocked inside `read()`, so `downloadTime` reflects true network read time and `parseTime` reflects only CPU parse cost; at higher log levels the original stream is used unchanged to avoid the per-`read()` overhead (snowflakedb/snowflake-jdbc#2640).
2728
- Fixed `Connection.isValid()` silently swallowing thread interruption: when the underlying heartbeat is interrupted, the connection's interrupt flag is now restored via `Thread.currentThread().interrupt()` so connection pools and Thread shutdown mechanisms can react to the interruption (snowflakedb/snowflake-jdbc#2314).
2829
- Fixed non-retryable HTTP 400 response bodies always being logged as `"Failed to read content due to exception: Attempted read from closed stream"`. The response entity is now buffered before `RestRequest#checkForDPoPNonceError` and `SnowflakeUtil#logResponseDetails` consume it so both readers see the body (snowflakedb/snowflake-jdbc#2631).
2930
- Added defense-in-depth canonical-path validation in the S3, Azure, and GCS download clients to ensure resolved local download paths cannot escape the user's GET target directory via traversal segments, absolute paths, or symlink redirection (snowflakedb/snowflake-jdbc#2623).

src/main/java/net/snowflake/client/internal/jdbc/SnowflakeChunkDownloader.java

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.fasterxml.jackson.databind.MappingJsonFactory;
88
import com.fasterxml.jackson.databind.ObjectMapper;
99
import java.io.ByteArrayOutputStream;
10+
import java.io.FilterInputStream;
1011
import java.io.IOException;
1112
import java.io.InputStream;
1213
import java.io.PrintWriter;
@@ -953,18 +954,26 @@ private static Callable<Void> getDownloadChunkCallable(
953954
* @throws SnowflakeSQLException
954955
*/
955956
private void downloadAndParseChunk(InputStream inputStream) throws SnowflakeSQLException {
956-
// remember the download time
957-
resultChunk.setDownloadTime(System.currentTimeMillis() - startTime);
958-
downloader.addDownloadTime(resultChunk.getDownloadTime());
959-
960-
startTime = System.currentTimeMillis();
961-
962-
// parse the result json
957+
// getResultStreamProvider().getInputStream() returns once headers come back, but the
958+
// response body is streamed lazily during the parser's read*() calls below — so most of
959+
// the true download time is spent inside read(), not before it. When the metrics log is
960+
// active (debug/trace), wrap the stream in a TimingInputStream so we can split the
961+
// download/parse times accurately. Otherwise, skip the per-read instrumentation and
962+
// fall back to the cheaper, less-accurate split (treating everything before the parse
963+
// call as download and everything inside it as parse) so we don't pay the wrapping cost
964+
// when nothing reads the result.
965+
boolean splitDownloadAndParseTimes = logger.isDebugEnabled() || logger.isTraceEnabled();
966+
long connectionSetupMs = System.currentTimeMillis() - startTime;
967+
TimingInputStream timedStream =
968+
splitDownloadAndParseTimes ? new TimingInputStream(inputStream) : null;
969+
InputStream parseStream = timedStream != null ? timedStream : inputStream;
970+
971+
long parseStart = System.currentTimeMillis();
963972
try {
964973
if (downloader.queryResultFormat == QueryResultFormat.ARROW) {
965-
((ArrowResultChunk) resultChunk).readArrowStream(inputStream);
974+
((ArrowResultChunk) resultChunk).readArrowStream(parseStream);
966975
} else {
967-
parseJsonToChunkV2(inputStream, resultChunk);
976+
parseJsonToChunkV2(parseStream, resultChunk);
968977
}
969978
} catch (Exception ex) {
970979
logger.debug(
@@ -997,9 +1006,22 @@ private void downloadAndParseChunk(InputStream inputStream) throws SnowflakeSQLE
9971006
}
9981007
}
9991008

1000-
// add parsing time
1001-
resultChunk.setParseTime(System.currentTimeMillis() - startTime);
1002-
downloader.addParsingTime(resultChunk.getParseTime());
1009+
long readPlusParseMs = System.currentTimeMillis() - parseStart;
1010+
long downloadMs;
1011+
long parseMs;
1012+
if (timedStream != null) {
1013+
long networkReadMs = TimeUnit.NANOSECONDS.toMillis(timedStream.getNanosBlocked());
1014+
downloadMs = connectionSetupMs + networkReadMs;
1015+
parseMs = Math.max(0L, readPlusParseMs - networkReadMs);
1016+
} else {
1017+
downloadMs = connectionSetupMs;
1018+
parseMs = readPlusParseMs;
1019+
}
1020+
1021+
resultChunk.setDownloadTime(downloadMs);
1022+
downloader.addDownloadTime(downloadMs);
1023+
resultChunk.setParseTime(parseMs);
1024+
downloader.addParsingTime(parseMs);
10031025
}
10041026

10051027
private long startTime;
@@ -1189,4 +1211,36 @@ public DownloaderMetrics terminate() {
11891211
return null;
11901212
}
11911213
}
1214+
1215+
/**
1216+
* InputStream wrapper that accumulates the time spent blocked inside read() calls. Used to
1217+
* separate true network read time from CPU-bound parsing time when reading a chunk.
1218+
*/
1219+
private static final class TimingInputStream extends FilterInputStream {
1220+
private long nanosBlocked = 0L;
1221+
1222+
TimingInputStream(InputStream in) {
1223+
super(in);
1224+
}
1225+
1226+
long getNanosBlocked() {
1227+
return nanosBlocked;
1228+
}
1229+
1230+
@Override
1231+
public int read() throws IOException {
1232+
long t0 = System.nanoTime();
1233+
int b = in.read();
1234+
nanosBlocked += System.nanoTime() - t0;
1235+
return b;
1236+
}
1237+
1238+
@Override
1239+
public int read(byte[] b, int off, int len) throws IOException {
1240+
long t0 = System.nanoTime();
1241+
int n = in.read(b, off, len);
1242+
nanosBlocked += System.nanoTime() - t0;
1243+
return n;
1244+
}
1245+
}
11921246
}

0 commit comments

Comments
 (0)