Database and runtime
A salsa database struct is declared by the user with the #[salsa::db]
annotation.
It contains all the data that the program needs to execute:
#[salsa::db(jar0...jarn)]
struct MyDatabase {
storage: Storage<Self>,
maybe_other_fields: u32,
}
This data is divided into two categories:
- Salsa-governed storage, contained in the
Storage<Self>
field. This data is mandatory. - Other fields (like
maybe_other_fields
) defined by the user. This can be anything. This allows for you to give access to special resources or whatever.
Parallel handles
When used across parallel threads, the database type defined by the user must support a "snapshot" operation.
This snapshot should create a clone of the database that can be used by the parallel threads.
The Storage
operation itself supports snapshot
.
The Snapshot
method returns a Snapshot<DB>
type, which prevents these clones from being accessed via an &mut
reference.
The Storage struct
The salsa Storage
struct contains all the data that salsa itself will use and work with.
There are three key bits of data:
- The
Shared
struct, which contains the data stored across all snapshots. This is primarily the ingredients described in the jars and ingredients chapter, but it also contains some synchronization information (a cond var). This is used for cancellation, as described below.- The data in the
Shared
struct is only shared across threads when other threads are active. Some operations, like mutating an input, require an&mut
handle to theShared
struct. This is obtained by using theArc::get_mut
methods; obviously this is only possible when all snapshots and threads have ceased executing, since there must be a single handle to theArc
.
- The data in the
- The
Routes
struct, which contains the information to find any particular ingredient -- this is also shared across all handles, and its construction is also described in the jars and ingredients chapter. The routes are separated out from theShared
struct because they are truly immutable at all times, and we want to be able to hold a handle to them while getting&mut
access to theShared
struct. - The
Runtime
struct, which is specific to a particular database instance. It contains the data for a single active thread, along with some links to shraed data of its own.
Incrementing the revision counter and getting mutable access to the jars
Salsa's general model is that there is a single "master" copy of the database and, potentially, multiple snapshots.
The snapshots are not directly owned, they are instead enclosed in a Snapshot<DB>
type that permits only &
-deref,
and so the only database that can be accessed with an &mut
-ref is the master database.
Each of the snapshots however onlys another handle on the Arc
in Storage
that stores the ingredients.
Whenever the user attempts to do an &mut
-operation, such as modifying an input field, that needs to
first cancel any parallel snapshots and wait for those parallel threads to finish.
Once the snapshots have completed, we can use Arc::get_mut
to get an &mut
reference to the ingredient data.
This allows us to get &mut
access without any unsafe code and
guarantees that we have successfully managed to cancel the other worker threads
(or gotten ourselves into a deadlock).
The code to acquire &mut
access to the database is the jars_mut
method:
#![allow(unused)] fn main() { /// Gets mutable access to the jars. This will trigger a new revision /// and it will also cancel any ongoing work in the current revision. /// Any actual writes that occur to data in a jar should use /// [`Runtime::report_tracked_write`]. pub fn jars_mut(&mut self) -> (&mut DB::Jars, &mut Runtime) { // Wait for all snapshots to be dropped. self.cancel_other_workers(); // Increment revision counter. self.runtime.new_revision(); // Acquire `&mut` access to `self.shared` -- this is only possible because // the snapshots have all been dropped, so we hold the only handle to the `Arc`. let shared = Arc::get_mut(&mut self.shared).unwrap(); // Inform other ingredients that a new revision has begun. // This gives them a chance to free resources that were being held until the next revision. let routes = self.routes.clone(); for route in routes.reset_routes() { route(&mut shared.jars).reset_for_new_revision(); } // Return mut ref to jars + runtime. (&mut shared.jars, &mut self.runtime) } }
The key initial point is that it invokes cancel_other_workers
before proceeding:
#![allow(unused)] fn main() { /// Sets cancellation flag and blocks until all other workers with access /// to this storage have completed. /// /// This could deadlock if there is a single worker with two handles to the /// same database! fn cancel_other_workers(&mut self) { loop { self.runtime.set_cancellation_flag(); // If we have unique access to the jars, we are done. if Arc::get_mut(&mut self.shared).is_some() { return; } // Otherwise, wait until some other storage entites have dropped. // We create a mutex here because the cvar api requires it, but we // don't really need one as the data being protected is actually // the jars above. // // The cvar `self.shared.cvar` is notified by the `Drop` impl. let mutex = parking_lot::Mutex::new(()); let mut guard = mutex.lock(); self.shared.cvar.wait(&mut guard); } } }
The Salsa runtime
The salsa runtime offers helper methods that are accessed by the ingredients.
It tracks, for example, the active query stack, and contains methods for adding dependencies between queries (e.g., report_tracked_read
) or resolving cycles.
It also tracks the current revision and information about when values with low or high durability last changed.
Basically, the ingredient structures store the "data at rest" -- like memoized values -- and things that are "per ingredient".
The runtime stores the "active, in-progress" data, such as which queries are on the stack, and/or the dependencies accessed by the currently active query.