Skip to content

Eliminate per-segment DistinctTable allocation and queue contention in DistinctCombineOperator - #19092

Open
Akanksha-kedia wants to merge 11 commits into
apache:masterfrom
Akanksha-kedia:feat/distinct-combine-thread-local
Open

Eliminate per-segment DistinctTable allocation and queue contention in DistinctCombineOperator#19092
Akanksha-kedia wants to merge 11 commits into
apache:masterfrom
Akanksha-kedia:feat/distinct-combine-thread-local

Conversation

@Akanksha-kedia

Copy link
Copy Markdown
Contributor

Description

DistinctCombineOperator previously followed the generic BaseSingleBlockCombineOperator queue-based pattern: each segment operator posted a DistinctResultsBlock to a LinkedBlockingQueue, and the main thread serially dequeued and merged them — one DistinctTable allocation and one queue round-trip per segment.

This PR replaces that with per-task thread-local accumulation, following the same design already used by GroupByCombineOperator:

Changes

DistinctCombineOperator

  • Each worker task claims a unique slot [0, numTasks) via AtomicInteger and merges every segment it processes directly into that slot's DistinctTable — no queue, no per-segment allocation.
  • After all tasks finish (signaled by CountDownLatch), the main thread merges at most numTasks per-task tables rather than numSegments tables.
  • Early termination sets _nextOperatorId to _numOperators as soon as a task's accumulated table satisfies the LIMIT, stopping all tasks.
  • CountDownLatch.countDown()→await() establishes the happens-before relationship that makes per-task table writes visible to the main thread (same guarantee used by GroupByCombineOperator).

DistinctCombineOperatorTest (new)

  • Full scan: verifies all numSegments × uniquePerSegment distinct values collected
  • Early termination: verifies fewer docs scanned when LIMIT is small
  • ORDER BY ASC/DESC: verifies correct sorted output
  • Cross-segment deduplication: verifies overlapping values across segments are merged correctly

Performance impact

For a SELECT DISTINCT col LIMIT L query over S segments with T worker threads (T << S):

  • Before: S DistinctTable allocations + S queue operations + S serial merges
  • After: T DistinctTable allocations + T final merges

The benefit is most pronounced for queries with a small LIMIT over many segments, where the bounded table size makes allocation cost non-trivial.

Test plan

  • DistinctCombineOperatorTest — 5 unit tests, all passing
  • Existing DistinctQueriesTest integration tests pass

Akanksha-kedia and others added 11 commits July 12, 2026 10:31
…validation

Implements a Trino/Ambari-style session layer for the Pinot controller UI that works
on top of any configured AccessControlFactory (BasicAuth, ZkBasicAuth, LDAP, or custom).

**Security problems fixed:**
- Credentials were visible in every browser network tab request (Authorization header)
- Logout only cleared localStorage — server session survived indefinitely
- No inactivity timeout; sessions never expired
- Credentials stored in JS memory (XSS-readable)
- Broker auth forwarded directly from the browser
- Race condition in session lookup (non-atomic get→check→remove)
- No CSRF protection on session cookie

**How it works:**
1. `POST /auth/login` validates credentials via the configured AccessControl, creates a
   server-side session, and sets an HttpOnly + SameSite=Strict cookie
2. `GET /auth/logout` immediately removes the session from the server store (proper invalidation)
3. `GET /auth/session` checks session validity for page-refresh restoration
4. `SessionAuthenticationFilter` (AUTHENTICATION-10 priority) validates the cookie before
   `AuthenticationFilter` runs; passes through requests with Authorization header for API compat
5. `PinotQueryResource` injects stored Basic-auth token server-side when forwarding to broker

**New files:**
- `SessionManager.java`: ConcurrentHashMap session store with sliding TTL, atomic compute()
- `SessionAuthenticationFilter.java`: JAX-RS filter for cookie-based auth
- `SessionBasicAuthAccessControlFactory.java`: convenience wrapper factory
- `PinotSessionLoginResource.java`: /auth/login, /auth/logout, /auth/session endpoints
- `SessionManagerTest.java`: unit + 50-thread concurrency test

**Configuration (all default-off for backward compatibility):**
```properties
controller.ui.session.authentication.enabled=true
controller.ui.session.cookie.secure=true       # set false for HTTP-only dev
controller.ui.session.inactivity.timeout.seconds=300  # default 5 min
```

**Frontend changes:**
- `Models.ts`: SESSION enum added to AuthWorkflow
- `axios-config.ts`: SESSION workflow deletes Authorization header (cookie sent automatically)
- `AuthProvider.tsx`: reads timeout from /auth/info, checks /auth/session on page refresh
- `App.tsx`: inactivity timer with 60-second warning dialog before auto-logout
- `Header.tsx`: real logout button that calls /auth/logout server-side
- `LoginPage.tsx`: SESSION path POSTs form data to /auth/login (no Basic header in JS)
- Remove trailing whitespace on blank lines in SessionAuthenticationFilter
- Remove blank lines before end-of-file in 4 new/modified files
- Replace Collections.emptyList/emptyMap with List.of/Map.of in PinotSessionLoginResource
- Fix LeftCurly violation in SessionManagerTest (synchronized block brace)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- SessionBasicAuthAccessControlFactory: replace non-existent BasicAuthUtils/
  AbstractBasicAuthAccessControl with correct BasicAuthPrincipalUtils and
  inline AccessControl implementation (mirrors BasicAuthAccessControlFactory)
- PinotQueryResource: fix @optional annotation package from
  org.glassfish.hk2.api to org.jvnet.hk2.annotations (correct HK2 location)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…return

AuthenticationFilter: replace early `return` with Basic-auth token injection
from the session store into the request headers, so validatePermission() and
FineGrainedAuthUtils still run with the session user's identity. Extract
isSessionEnabled() helper to consolidate the repeated config check.

PinotQueryResource: remove `if (!sessionValid)` guards around hasAccess()
calls in getQueryResponse() and getMultiStageQueryResponse(). Now that
AuthenticationFilter injects the Authorization header for session requests,
the downstream hasAccess() calls receive the session user's credentials and
work correctly. Remove hasValidSessionCookie() and sessionValid parameter.

ControllerConf: shorten multi-line session constant Javadocs to one-liners.

Addresses CRITICAL review feedback from @xiangfu0 and @shounakmk219 on PR apache#18933.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…roker auth

ControllerAdminApiApplication: revert CORS to wildcard-only (no credentials).
The Pinot UI is same-origin so CORS is not needed; the previous origin-reflect
+ Allow-Credentials approach was too permissive for same-site sibling origins.

PinotSessionLoginResource: use hasAccess(AccessType, headers, url) instead of
hasAccess(null, AccessType, headers, url). The table-scoped overload calls
hasTable(null) which rejects principals with a table allowlist, locking them
out of session login entirely. The identity-only overload validates credentials
without requiring any table permission.

PinotQueryResource: extract injectSessionCredentialIfNeeded() helper and call
it in both sendRequestToBroker() and sendTimeSeriesRequestToBroker(). Previously
only the SQL forward path re-injected the session credential after extractHeaders()
stripped it; time-series requests were forwarded without broker auth in session mode.

Addresses review feedback from @xiangfu0 on PR apache#18933.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…fault impl

Extract SessionManager as an interface (keeping SESSION_COOKIE_NAME and
DEFAULT_SESSION_TTL_SECONDS constants + the public API) and rename the
in-memory implementation to InMemorySessionManager. All injection points
(AuthenticationFilter, SessionAuthenticationFilter, PinotQueryResource,
PinotSessionLoginResource) already reference the SessionManager type so they
are unaffected. BaseControllerStarter now instantiates InMemorySessionManager.
SessionManagerTest updated to construct InMemorySessionManager directly while
keeping variable types as SessionManager.

Addresses @shounakmk219 review feedback on PR apache#18933.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
… API clients

extractHeaders was stripping the Authorization header from every request
when session mode was globally enabled, including API clients that supply
their own Basic/Bearer token with no session cookie. injectSessionCredentialIfNeeded
only re-injected if a session cookie was present, so API clients had their
auth silently removed before the broker call.

Merge the two-step strip+inject into a single buildBrokerHeaders call that
resolves the session credential exactly once via resolveSessionCredential.
If a valid session cookie is found, the caller Authorization is replaced with
the stored token; otherwise all headers are forwarded unchanged.
…ticky-session requirement

Three issues raised in review:

1. TTL mismatch: InMemorySessionManager used a sliding-window TTL (getUsername
   extended expiry on each call) while the browser cookie Max-Age was fixed at
   login. An active session could outlive its browser cookie, causing unexpected
   logouts. Fixed by removing the sliding extension in getUsername; both server
   session and cookie now expire at loginTime + TTL.

2. Cross-controller logout: InMemorySessionManager is per-process, so logout
   routed to a different controller in an LB setup silently fails. Added a
   comment in BaseControllerStarter documenting the sticky-session requirement
   and the degraded-but-functional behavior without it.

3. CORS allowed headers: Access-Control-Allow-Headers was missing the custom
   Pinot 'Database' header used by DatabaseUtils for multi-database table
   resolution. Also adds If-Match for segment-upload endpoints. Browser clients
   that set these headers were failing preflight.
…actory, escapeJson, timer leak

- AuthenticationFilter: also detect SESSION workflow from factory.getAuthWorkflowInfo()
  so isSessionEnabled() works even when CONTROLLER_UI_SESSION_ENABLED flag is absent.
  Cache result in volatile Boolean to avoid calling factory.create() on every request.
  Add auth/session/renew to UNPROTECTED_PATHS so the renew endpoint is reachable without
  an Authorization header.

- PinotSessionLoginResource: replace non-atomic get/invalidate/create with rotateSession()
  to eliminate TOCTOU in POST /auth/session/renew. Add escapeJson() and apply to all three
  username-in-JSON response sites. Fix buildDeleteCookieHeader to propagate the Secure flag.

- SessionManager: add rotateSession() default method (non-atomic fallback for custom impls).

- InMemorySessionManager: override rotateSession() with CAS-based atomic rotation using
  ConcurrentHashMap.remove(key, value) to prevent double-rotation under concurrent requests.

- PinotControllerAuthResource: enrich AuthWorkflowInfo with inactivity timeout when factory
  advertises SESSION workflow, not just when the explicit config flag is set.

- App.tsx: change "Stay Logged In" to POST /auth/session/renew (was incorrectly hitting
  GET /auth/session which doesn't renew server-side TTL). Fix performSessionLogout to also
  clear the countdown interval, preventing warningCountdown going negative after logout.
…n DistinctCombineOperator

Replace the one-result-block-per-segment / BlockingQueue pattern with per-task
thread-local DistinctTable accumulation, following the same design as
GroupByCombineOperator:

- Each worker task is assigned a unique slot (0..numTasks-1) and merges every
  segment it processes directly into that slot's DistinctTable, eliminating
  both the per-segment DistinctTable allocation and the LinkedBlockingQueue
  offer/poll overhead.
- After all tasks finish (via CountDownLatch), the main thread merges at most
  numTasks tables instead of numSegments tables.
- Early termination sets _nextOperatorId to _numOperators to stop all tasks
  as soon as the LIMIT is satisfied, consistent with the existing pattern.
- Adds DistinctCombineOperatorTest covering full-scan, early termination,
  ORDER BY ASC/DESC, and cross-segment deduplication.
@Akanksha-kedia

Copy link
Copy Markdown
Contributor Author

cc @Jackie-Jiang @walterddr @richardstartin — would appreciate a review when you get a chance!

@codecov-commenter

codecov-commenter commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 18.40659% with 297 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.84%. Comparing base (a2a27a1) to head (b3439a7).
⚠️ Report is 74 commits behind head on master.

Files with missing lines Patch % Lines
...oller/api/resources/PinotSessionLoginResource.java 0.00% 113 Missing ⚠️
...core/operator/combine/DistinctCombineOperator.java 0.00% 57 Missing ⚠️
...roller/api/access/SessionAuthenticationFilter.java 10.81% 31 Missing and 2 partials ⚠️
...i/access/SessionBasicAuthAccessControlFactory.java 0.00% 24 Missing ⚠️
.../controller/api/access/InMemorySessionManager.java 68.49% 21 Missing and 2 partials ⚠️
...t/controller/api/resources/PinotQueryResource.java 0.00% 17 Missing ⚠️
...ler/api/resources/PinotControllerAuthResource.java 0.00% 9 Missing ⚠️
...ot/controller/api/access/AuthenticationFilter.java 38.46% 4 Missing and 4 partials ⚠️
...che/pinot/controller/api/access/AccessControl.java 12.50% 7 Missing ⚠️
...he/pinot/controller/api/access/SessionManager.java 0.00% 6 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (a2a27a1) and HEAD (b3439a7). Click for more details.

HEAD has 8 uploads less than BASE
Flag BASE (a2a27a1) HEAD (b3439a7)
java-21 5 0
unittests1 1 0
unittests 2 1
custom-integration1 1 0
Additional details and impacted files
@@              Coverage Diff              @@
##             master   #19092       +/-   ##
=============================================
- Coverage     65.05%   37.84%   -27.22%     
- Complexity     1403     1420       +17     
=============================================
  Files          3399     3435       +36     
  Lines        212808   218354     +5546     
  Branches      33568    34706     +1138     
=============================================
- Hits         138443    82626    -55817     
- Misses        63223   128128    +64905     
+ Partials      11142     7600     -3542     
Flag Coverage Δ
custom-integration1 ?
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-21 ?
java-25 37.84% <18.40%> (?)
temurin 37.84% <18.40%> (-27.22%) ⬇️
unittests 37.83% <18.40%> (-27.22%) ⬇️
unittests1 ?
unittests2 37.83% <18.40%> (+0.45%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Akanksha-kedia

Copy link
Copy Markdown
Contributor Author

Could a maintainer please add the enhancement and performance labels to this PR? Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants