refactor(auth): move USER_LAST_ACTIVE_TIME write out of JwtAuthFilter#4888
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4888 +/- ##
============================================
+ Coverage 43.13% 44.05% +0.92%
Complexity 2155 2155
============================================
Files 914 958 +44
Lines 32196 34063 +1867
Branches 3253 3756 +503
============================================
+ Hits 13887 15008 +1121
- Misses 17466 18302 +836
+ Partials 843 753 -90
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
e96d302 to
29b101e
Compare
JwtAuthFilter previously did a synchronous USER_LAST_ACTIVE_TIME upsert on every authenticated request, coupling JWT verification to a per- request DB round-trip and mixing user-management concerns into the auth pipeline. Strip the DB write out of the filter. The filter is now pure: extract token, verify, set SecurityContext. Add a UserActivityTracker in common/auth that throttles per-uid DB writes by an in-memory cooldown (default 5 minutes) and runs the upsert on a single-thread daemon executor so request threads never wait on DB latency. Tracker is in common/auth so the throttle/CAS logic and its tests live with the rest of auth-adjacent code. Add a UserActivityEventListener (Jersey ApplicationEventListener) in access-control-service. The listener observes RESOURCE_METHOD_FINISHED at Jersey's monitoring layer (no ContainerRequestFilter semantics, cannot reject or transform requests) and forwards uid to the tracker. Listener lives only in access-control-service: USER_LAST_ACTIVE_TIME is a user-management concern, and authenticated client sessions necessarily contact this service often (UI navigation, permission checks, LiteLLM proxy) so other services do not need to mirror this listener. Other services keep their JwtAuthFilter registration but no longer incidentally write to USER_LAST_ACTIVE_TIME. Tests: 5-case spec for tracker cooldown/CAS in common/auth, 5-case spec for listener trigger gating in access-control-service. Both run synchronously without DB or Jersey runtime via injected upsert function and Mockito-based RequestEvent stubs. Closes apache#4887
29b101e to
2080c42
Compare
There was a problem hiding this comment.
Pull request overview
Refactors user activity tracking so JWT authentication no longer performs a per-request DB upsert, and moves USER_LAST_ACTIVE_TIME writes behind a throttled, asynchronous tracker invoked from access-control-service’s Jersey monitoring layer.
Changes:
- Remove
USER_LAST_ACTIVE_TIMEupsert fromJwtAuthFilter, leaving it as a pure JWT-to-SecurityContextfilter. - Add
UserActivityTracker(throttled per-uid, async writer) plus unit tests incommon/auth. - Add
UserActivityEventListenerin access-control-service to trigger tracking onRESOURCE_METHOD_FINISHED, plus unit tests and service registration.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala | Removes synchronous activity DB write from auth filter. |
| common/auth/src/main/scala/org/apache/texera/auth/UserActivityTracker.scala | Introduces throttled async activity upsert singleton + testable class form. |
| common/auth/src/test/scala/org/apache/texera/auth/UserActivityTrackerSpec.scala | Adds unit coverage for cooldown/CAS behavior. |
| common/auth/build.sbt | Adds ScalaTest dependency for new common/auth tests. |
| access-control-service/src/main/scala/org/apache/texera/service/activity/UserActivityEventListener.scala | Adds Jersey monitoring listener to invoke tracking after resource method completes. |
| access-control-service/src/test/scala/org/apache/texera/service/activity/UserActivityEventListenerSpec.scala | Adds unit tests for listener behavior across event/principal cases. |
| access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala | Registers the new listener in access-control-service. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Wrap upsertFn invocation inside the executor task in try/NonFatal so injected implementations cannot leak exceptions, with full stack trace logged. - Replace catch Throwable with NonFatal in defaultUpsert so OOME and the like still propagate, and pass the exception to logger to keep the stack trace. - Add evictStale() with TTL cleanup at 2 * writeInterval; schedule a daemon ScheduledExecutorService in the production singleton to run it once per WRITE_INTERVAL. Bounds lastClaimed for long-lived processes with many distinct uids. - Replace newSingleThreadExecutor (unbounded queue) with a bounded ThreadPoolExecutor (capacity 256) and DiscardOldestPolicy. Under DB stalls or write storms, oldest pending tasks are dropped; the next request from the same uid will re-claim and re-write once cooldown elapses. Adds two tests: evictStale eviction window, and exception swallowing on a throwing upsertFn.
Refactor UserActivityEventListener: - Inner anonymous RequestEventListener becomes a SAM-converted lambda, removing the anonymous-class boilerplate that jacoco was reporting branch-partials on. - Per-event branching moves to a top-level companion method `handle`, visible to tests so the per-event logic is exercised without going through the SAM dispatch. - Replace `event.getType == ...` with `event.getType eq ...`. The Type is a Java enum (singletons), so reference equality is semantically correct and compiles to a single if_acmpne, avoiding Scala's BoxesRunTime.equals branch fan-out (the partial-coverage noise on the comparison line). Tests: - UserActivityEventListenerSpec rewritten to drive `handle` directly for the per-event cases (5), plus 3 listener-level smoke tests for the SAM dispatch / lifecycle / default-arg ctor paths. - New AccessControlServiceRunSpec verifies UserActivityEventListener is registered when run() is invoked. Mocks Environment, so this also incidentally covers the rest of run() — no longer at 0%.
What changes were proposed in this PR?
JwtAuthFilterdid a synchronousUSER_LAST_ACTIVE_TIMEupsert on every authenticated request — coupling JWT verification to a DB round-trip and mixing user-management into the auth pipeline. Strip the DB write out of the filter.The replacement:
UserActivityTracker(incommon/auth) — per-uid in-memory cooldown (5 min default) + single-thread daemon executor, so request threads never wait on DB.UserActivityEventListener(in access-control-service) — a JerseyApplicationEventListener, not aContainerRequestFilter. It observesRESOURCE_METHOD_FINISHEDand forwards the uid to the tracker. Cannot reject or transform requests.Listener is registered only in access-control-service. Other services keep
JwtAuthFilterbut no longer writeUSER_LAST_ACTIVE_TIME— authenticated client sessions necessarily contact access-control-service often enough (UI navigation, permission checks, LiteLLM proxy) to capture activity with high recall.Any related issues, documentation, discussions?
Closes #4887
How was this PR tested?
Unit specs cover tracker cooldown/CAS (synchronous executor + injectable clock, no DB) and listener gating (Mockito stubs for
RequestEvent, no Jersey runtime).Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.7)