Repository navigation
CloudFetch and M2M token exchange are unbounded and uninterruptible (v1.13.0) (#475) - #522
peco-engineer-bot[bot] wants to merge 12 commits into
Conversation
…v1.13.0) (#475) Signed-off-by: peco-engineer-bot[bot] <3815206+peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Medium · 1 Low · 2 Nit
The fix is mostly sound, with 1 medium concern: a negative WithCloudFetchMinTimeToExpiry value skips the default clamp and makes the expiry check accept links that have already expired. The other findings are low-severity: the new options are silently ignored on the kernel backend, the mutex change has no test, and the token-exchange transport is hard-coded.
Other findings
- ⚪ Nit — The lock-scope change (no longer holding
c.mxacross a cachedts.Token()) has no test.TestM2MTokenExchangeIsBoundedsetsc.tokenSourcedirectly and callsAuthenticateonce, so it only checks the HTTP timeout. A regression back to holding the lock for the whole call would still pass. A test that runs two concurrentAuthenticatecalls against a slow token server and checks that both finish in about one timeout (or that the second one isn't serialized behind the first) would pin this behavior. Separately, the first-fetch path (tokenSource == nil) still holdsc.mxthroughGetConfigplusToken(). That is bounded now (10s + 30s), but other callers can still be blocked for up to about 40s. - ⚪ Nit — The token-exchange client is a fresh
&http.Client{Timeout: ...}, which useshttp.DefaultTransport. So the M2M token request ignores anyWithTransport/ TLS / proxy settings on the connector, same as before. Pinning the timeout in now makes it a fixed 30s that callers can't tune. That's fine as a safe default, but if a corporate proxy or a custom TLS setup needs the connector's transport, this path can't use it. Consider at least documenting the limitation onGetTokenSource.
Addresses: - #4419179136 at connector.go:552 - #4419179140 at connector.go:542 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
Looks good, with one low-severity edge case: passing nil to WithCloudFetchHTTPClient wipes the CloudFetch client that WithTransport set up. The M2M timeout and the change to stop holding the mutex during a cached token refresh are correct and covered by tests that fail without the fix. The e2e tests still need a maintainer to run them against a live warehouse with credentials, as the description says.
Addresses: - #4419227934 at connector.go:548 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 2 Low
Looks good overall. The two ConnOptions are correct, WithTransport only fills HTTPClient when it's nil so precedence works in either option order, and cfg.HTTPClient is only used by the CloudFetch batch loader. The 30s token-exchange client timeout fixes the hang. I left two low-severity notes: the mutex change doesn't do what its comment claims, and the test doesn't cover the first-call Authenticate branch. The e2e tests still need a maintainer run with credentials, as the PR description says.
Addresses: - #4419266219 at auth/oauth/m2m/m2m.go:61 - #4419266226 at auth/oauth/m2m/m2m_test.go:17 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Medium · 1 Low
The new ConnOptions behave as their docs say, and the precedence and nil handling are tested. The M2M token exchange now has a timeout. One medium issue: the PR description says the Authenticate mutex was narrowed, but the code still holds it across Token().
Addresses: - #4419309782 at auth/oauth/m2m/m2m.go:94 - #4419309786 at auth/oauth/m2m/m2m.go:97 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
Looks good overall, with one low-severity issue: a comment in m2m.go misdescribes how concurrent token refreshes behave. The ConnOption precedence logic matches its docs and tests. The token-exchange timeout bound is correct and has offline tests that would fail without the fix. As the PR notes, the two e2e tests still need a run with live credentials.
|
|
||
| c.tokenSource = GetTokenSource(config) | ||
| token, err := c.tokenSource.Token() | ||
| // c.mx is not held here: the oauth2.ReuseTokenSource returned by |
There was a problem hiding this comment.
🔵 Low — The new comment says that because c.mx is no longer held, "a slow token exchange does not serialize every request on the connector". That isn't accurate. The ReuseTokenSource returned by clientcredentials.Config.TokenSource holds its own mutex for the whole refresh (s.mu.Lock(); … s.new.Token()). So every concurrent Authenticate on this connector still waits behind a slow or hung exchange. They just wait on oauth2's lock now instead of c.mx. The fix that actually works is the tokenExchangeTimeout bound in GetTokenSource. Dropping c.mx on the cached path changes nothing for concurrency. Please reword the comment (and the PR description's claim) so a later reader doesn't count on non-serialization that isn't there. Something like: "c.mx only guards lazy init; concurrent refreshes are still serialized by ReuseTokenSource, but each is bounded by tokenExchangeTimeout."
There was a problem hiding this comment.
⛔ NEEDS HUMAN DECISION — the bots can't resolve this thread; a maintainer's input is required.
I agree, and the code comment already says this at HEAD, so I made no edit. In auth/oauth/m2m/m2m.go, the comment in Authenticate just above ts.Token() now reads: "c.mx guards only the lazy init above. Token refreshes are still serialized inside the oauth2.ReuseTokenSource (it holds its own mutex for the whole exchange), so concurrent callers wait on an in-flight refresh; that wait is bounded by tokenExchangeTimeout." The comment on getOrInitTokenSource also says c.mx guards only the lazy init, and the tokenExchangeTimeout doc names the timeout as the actual fix. The PR description still makes the "does not serialize every request" claim, and I can't edit it from here. A human needs to reword that line to match the code comment.
Addresses: - #4419266219 at auth/oauth/m2m/m2m.go:61 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
Looks good, with 1 low-severity doc concern about how Client.Timeout interacts with CloudFetch retries. The precedence between the new ConnOption and WithTransport holds because WithTransport only sets HTTPClient when it is nil. The M2M lock narrowing is safe because oauth2.ReuseTokenSource still serializes refreshes. Nit: the PR lists internal/rows/arrowbased/arrowRows.go as a touched file, but the diff doesn't change it, so the description should be corrected.
Addresses: - #4419385758 at connector.go:539 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
Looks good, with one low concern: the M2M exchange can take about twice the documented bound because of oauth2's AuthStyle auto-detect retry. I checked that the precedence between WithCloudFetchHTTPClient and WithTransport holds in both option orders (WithTransport only sets HTTPClient when it is nil). The lazy init under the mutex and the token refresh outside it look correct.
Addresses: - #4419421372 at auth/oauth/m2m/m2m.go:117 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Medium · 1 Low
Looks good overall, with one medium doc/contract concern. The new WithCloudFetchHTTPClient doc tells users to rely on the query context for an overall deadline, but rows deliberately strip the caller's cancellation from download contexts. Nit: the PR description lists internal/rows/arrowbased/arrowRows.go under Files, but the diff doesn't touch it.
Addresses: - #4419449961 at connector.go:549 - #4419449969 at connector.go:576 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
Looks good, with one low-severity concern. The new ConnOptions do what their doc comments say, which I checked against batchloader.go (retry loop, per-attempt timeout, one-second expiry resolution, cfg.HTTPClient used only for CloudFetch). The M2M change is also correct: the lazy init is locked, refreshes are serialized by ReuseTokenSource, and the timeout is bounded per request. The e2e tests still need a maintainer run with credentials before merge, as the PR description says.
Addresses: - #4419483095 at auth/oauth/m2m/m2m.go:124 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
Looks good: the new ConnOptions are wired correctly, and the M2M timeout and lock-scope changes are sound. They match how the retry loop in fetchBatchBytes and the mutex inside oauth2.ReuseTokenSource actually behave. One low finding: an e2e assertion that can never fail. One nit: the PR description lists internal/rows/arrowbased/arrowRows.go as a changed file, but the diff doesn't touch it.
Addresses: - #4419516870 at driver_e2e_test.go:842 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
Looks good, with one low-severity contract note. Option precedence (WithTransport sets HTTPClient only when it is nil), the retry/expiry wording in the doc comments (matches batchloader.go:404-453), and the M2M lock narrowing (ReuseTokenSource still serializes refreshes) all check out. Nit: the PR description lists internal/rows/arrowbased/arrowRows.go as a changed file, but the diff doesn't touch it. The root-cause section explains why the context change was dropped, so the file list should be updated to match.
Addresses: - #4419547396 at auth/oauth/m2m/m2m.go:127 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Summary
Automated fix for #475 — CloudFetch and M2M token exchange are unbounded and uninterruptible (v1.13.0).
Added two new ConnOptions in connector.go.
WithCloudFetchHTTPClientsets the CloudFetch download client, so callers can sethttp.Client.Timeoutwithout replacing the Thrift transport; it wins overWithTransportwhichever comes first.WithCloudFetchMinTimeToExpirysetsMinTimeToExpiryand its comment says an expired link ends the read for good. In auth/oauth/m2m,GetTokenSourcenow uses anhttp.Clientwith a timeout (tokenExchangeTimeout, default 30s), andAuthenticateno longer holds the mutex during a cached token refresh. The unit testsTestCloudFetchConnOptionsandTestM2MTokenExchangeIsBoundedpass, andmake testandmake lint(0 issues) are green. The two e2e tests now compile, but they SKIP in this shell because the tool scrubs theDATABRICKS_PECOTESTING_*credentials, so I could not see them pass against the live warehouse. A maintainer should runTestE2ECloudFetchMinTimeToExpiryIsHonoredandTestE2ECloudFetchHTTPClientOptionwith creds before merging. The M2M part is offline-only: a live warehouse can't be made to hang its token endpoint. The expected behavior comes from the issue, which asks for the exchange to be bounded like the 10s endpoint-discovery cap.Root cause & plan
Root cause: The issue has five parts. I checked each against the code:
(1) Context:
arrowRows.go:126/128does passcontext.Background()toNewCloudBatchIterator/NewLocalBatchIteratorinstead of the scanner'sctx. But thectxthe scanner gets isrows.ctx, whichNewRowsdeliberately detaches from the caller withcontext.WithoutCancel(ES-1934053 / #295 / #371). The existing e2e testTestE2EArrowBatchesSurviveQueryContextCancellationprotects that. So "the caller's QueryContext cancellation should stop downloads" goes against a documented design decision. Passingctxthrough only keeps the context values (conn, correlation and query IDs) and ties downloads toresultsCancel.rows.Closealready cancels downloads throughRowScanner.Close, so there is nothing that fails today to test.(2) The download client is
http.DefaultClient, which has no timeout. The only way to setcfg.HTTPClientisWithTransport, which also replaces the Thrift transport. There is noConnOptionfor the CloudFetchhttp.Client, so a caller cannot sethttp.Client.Timeout.(3)
MinTimeToExpiryalso has noConnOption. An expired link returns the terminal errorerrors.New(ErrLinkExpired)and nothing re-issues links.(4)
m2m.authClient.Authenticateholdsc.mxacrosstokenSource.Token().GetTokenSourcebuilds the token source withcontext.Background(), soclientcredentialsuseshttp.DefaultClientwith no timeout. A hung token endpoint therefore blocksAuthenticate, and every request on the connector, indefinitely. Endpoint discovery is capped at 10s (oauth.hostConfigTimeout); the token exchange is not.(5) Doc mismatch: already fixed.
connector.gonow says "WithCloudFetch ... Default is true."Files:
connector.go,auth/oauth/m2m/m2m.go,internal/rows/arrowbased/arrowRows.go,driver_e2e_test.go,connector_test.go,auth/oauth/m2m/m2m_test.goPlanned coverage:
SELECT * FROM range(0, 5000000)with WithCloudFetch(true), using a newWithCloudFetchMinTimeToExpiry(24*time.Hour)ConnOption. That margin is longer than any real presigned URL's lifetime. Iterate the rows and assert iteration fails with an error containing dbsqlerr.ErrLinkExpired. Running the same query without the option must return the exact row count. Red today: the option doesn't exist, the setting can't be reached, andcfg.MinTimeToExpirystays 0. The test exercises the real server-issuedlink.ExpiryTime. (Fix the module name #3 MinTimeToExpiry cannot be set and has no effect on live CloudFetch downloads)WithCloudFetchHTTPClient(&http.Client{Timeout: 2*time.Minute, Transport: countingRT}). countingRT wraps http.DefaultTransport and counts requests. Run the large CloudFetch query, read every row, and assert: the exact row count, countingRT saw at least one request (the presigned-URL GETs went through the supplied client), and every counted request host differs from the workspace host (the Thrift transport was not replaced). Add a connector_test.go unit test: the option sets cfg.HTTPClient (same pointer, Timeout preserved) and leaves cfg.Transport nil. Also add one where WithTransport and WithCloudFetchHTTPClient are both given in either order; the explicit client must win. (Update references to new repo location #2 no way to give CloudFetch downloads their own http.Client (and so a Timeout) without replacing the Thrift transport)oauth2.HTTPClient= &http.Client{Timeout: tokenExchangeTimeout} (default about 10–30s), so each exchange is bounded. (Revert "Fix the module name" #4 M2M token exchange is unbounded: a hung token endpoint blocks Authenticate forever)Files changed
driver_e2e_test.goconnector_test.goauth/oauth/m2m/m2m_test.goconnector.goauth/oauth/m2m/m2m.goTest plan
TestCloudFetchConnOptions— fails (red) against the original code, passes (green) after the fixTestM2MTokenExchangeIsBounded— fails (red) against the original code, passes (green) after the fix🤖 Generated by engineer-bot (bug-fix flow) — review before merge.
Not changed in this PR (out of scope)
These facets could not be fixed here. Reason given for each — please follow up as needed: