Resolve RBAC deadlocks via sequential locking by ygndotgg · Pull Request #1685 · parseablehq/parseable · GitHub
Skip to content

Resolve RBAC deadlocks via sequential locking - #1685

Open
ygndotgg wants to merge 1 commit into
parseablehq:mainfrom
ygndotgg:rbac_deadlock
Open

Resolve RBAC deadlocks via sequential locking#1685
ygndotgg wants to merge 1 commit into
parseablehq:mainfrom
ygndotgg:rbac_deadlock

Conversation

@ygndotgg

@ygndotgg ygndotgg commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Description

Resolves critical RwLock deadlocks in the in-memory RBAC maps (USERS, SESSIONS, ROLES, USER_GROUPS) that caused the server to freeze under concurrent user management and authorization requests.

Possible solutions and chosen one

A global lock-ordering hierarchy could have been implemented, but the chosen solution enforces a strict Core Invariant: a map lock may only read or mutate its own map. Cross-map logic is achieved by cloning lightweight data (e.g., the AuthSnapShot struct), dropping the current lock, and acquiring the next map sequentially. This minimizes lock hold times and completely eliminates circular lock dependencies.

Key changes made in the patch

  • Read Path: Replaced the nested-lock Sessions::check_auth with an AuthSnapShot pattern to drop the SESSIONS read lock before evaluating cross-map permissions.
  • Mutation Paths: Applied a "3-Step Drop" to add_roles, remove_roles, change_password_hash, put_user, and delete_user to ensure USERS is fully unlocked before touching SESSIONS or ROLES.
  • Aggregation Path: Refactored aggregate_group_permissions and roles_to_permission to explicitly drop USER_GROUPS and ROLES read locks between loop iterations.

This PR has:

  • been tested to ensure log ingestion and log query works.
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added documentation for new or modified features or behaviors.

Summary by CodeRabbit

  • Refactor
    • Updated authorization to use a snapshot of user permissions for more efficient, consistent permission checks.
    • Improved how session permissions are computed and refreshed after role changes.
    • Refined user/session handling during password and user updates to ensure session state stays in sync.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/rbac/mod.rs (1)

336-344: 💤 Low value

Narrow race window between session insertion and snapshot retrieval.

After track_new completes and the write lock is dropped (line 335), there's a brief window before the read lock is acquired (line 337) where another thread could theoretically remove the session. The .expect() on line 344 would panic in this case.

While this window is extremely narrow and would require a concurrent remove_user call for the same user being authorized, consider using .unwrap_or(Response::ReloadRequired) instead of .expect() for defensive resilience.

🛡️ Defensive alternative
-                return snapshot
-                    .map(|snap| {
-                        map::check_auth_snapshot(snap, action, context_stream, context_user)
-                    })
-                    .expect("entry for this key just added");
+                return snapshot
+                    .map(|snap| {
+                        map::check_auth_snapshot(snap, action, context_stream, context_user)
+                    })
+                    .unwrap_or(Response::ReloadRequired);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rbac/mod.rs` around lines 336 - 344, Replace the `.expect("entry for this
key just added")` call on the snapshot map chain with
`.unwrap_or(Response::ReloadRequired)` to defensively handle the rare race
condition where another thread might remove the session entry between the write
lock being dropped after track_new and the read lock being acquired in
sessions(). This prevents a panic if the session snapshot is no longer available
when retrieved.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/rbac/mod.rs`:
- Around line 336-344: Replace the `.expect("entry for this key just added")`
call on the snapshot map chain with `.unwrap_or(Response::ReloadRequired)` to
defensively handle the rare race condition where another thread might remove the
session entry between the write lock being dropped after track_new and the read
lock being acquired in sessions(). This prevents a panic if the session snapshot
is no longer available when retrieved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c2e5ee68-0278-47f6-b02f-f9658734d92c

📥 Commits

Reviewing files that changed from the base of the PR and between 7d1d9a9 and caae924.

📒 Files selected for processing (2)
  • src/rbac/map.rs
  • src/rbac/mod.rs

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 17, 2026
Eliminated nested `RwLock` acquisitions by introducing `AuthSnapShot`
for read paths and sequential lock-dropping for user mutations.
Server remains deadlock-free under `quest-parallel` concurrent loads.
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.

1 participant