Skip to content

Make it impossible for background threads to mutate Salsa inputs#1314

Open
lionel- wants to merge 1 commit into
mainfrom
oak/analysis-db
Open

Make it impossible for background threads to mutate Salsa inputs#1314
lionel- wants to merge 1 commit into
mainfrom
oak/analysis-db

Conversation

@lionel-

@lionel- lionel- commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Part of #1212

This approach is taken from Rust-Analyzer where a snapshot is guaranteed to never write to the DB. Having this type-enforced safety is nice because writing to the DB from outside the main loop could deadlock.

  • New WorldSnapshot type that's the read-only counterpart to WorldState. It holds a clone of the Salsa data wrapped in Analysis, as well as clones of other world state fields.

  • New Analysis type that holds the OakDatabase in a private field, and lends it only as a shared reference. This is what prevents writes.

@DavisVaughan DavisVaughan 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.

I really really like the idea a lot, but the naming is throwing me off rn

Comment on lines +20 to +25
/// The main loop owns and mutates this. Background readers get a
/// [`WorldSnapshot`] instead, which holds a read-only [`Analysis`] in place of
/// the writable `OakDatabase`. This prevents background threads from reaching a
/// Salsa input setter. See [`Self::diagnostics_snapshot`] and
/// [`Self::snapshot`]. This split mirrors rust-analyzer's `GlobalState` and
/// `GlobalStateSnapshot`.

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.

Suggested change
/// The main loop owns and mutates this. Background readers get a
/// [`WorldSnapshot`] instead, which holds a read-only [`Analysis`] in place of
/// the writable `OakDatabase`. This prevents background threads from reaching a
/// Salsa input setter. See [`Self::diagnostics_snapshot`] and
/// [`Self::snapshot`]. This split mirrors rust-analyzer's `GlobalState` and
/// `GlobalStateSnapshot`.
/// The main loop owns and mutates this. Background readers get a
/// [`WorldStateSnapshot`] instead, which holds a read-only [`OakDatabaseSnapshot`] in place of
/// the writable `OakDatabase`. This prevents background threads from reaching a
/// Salsa input setter. See [`Self::diagnostics_snapshot`] and
/// [`Self::snapshot`]. This split mirrors rust-analyzer's `GlobalState` and
/// `GlobalStateSnapshot`.

I like this idea a lot, but I find the names to be very confusing in the long term.

Like this used to make sense

state: WorldState,

but no longer does to me as

state: WorldSnapshot,

I would prefer

state: WorldStateSnapshot,

And Analysis doesn't really mean much to me at all, I would definitely like to see that tied to the OakDatabase name in some way. I'd be fine with OakDatabaseSnapshot or ReadOnlyOakDatabase.

Like seeing pub(crate) db: Analysis, just confuses me naming wise :/

Comment thread crates/ark/src/lsp/db.rs
Comment on lines +20 to +29
/// Read-only handle to an `OakDatabase` that acts as a read-only snapshot.
///
/// An `Analysis` snapshot holds the DB clone in a private field and lends a
/// shared ref via `read()`. This prevents a read-only background workers
/// from reaching DB setters and deadock with the main loop. This is oak's
/// version of rust-analyzer's `Analysis` facade.
#[derive(Clone, Debug)]
pub(crate) struct Analysis {
db: OakDatabase,
}

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.

I have a gut feeling this is too complex for us

What if we just had

pub(crate) struct WorldStateSnapshot {
    // Private field. We only give out `&dyn ArkDb` handles on threads to enforce that it is read-only.
    db: OakDatabase
}

impl WorldStateSnapshot {
    /// Read-only access to the `OakDatabase`. Important that we return as `&dyn ArkDb`
    /// to prevent the thread from simply `.clone()`ing to get its own copy that it can call
    /// setters with.
    pub(crate) db(&self) -> &dyn ArkDb {
        &self.db
    }
}

Your little test only cancellation_token() could live on WorldStateSnapshot, I think.

This setup makes a lot of sense to me! And avoids a lot of the layers that seem to make naming this thing hard.

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