Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce RefUnwindSafe StorageHandle #675

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 75 additions & 32 deletions src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Public API facades for the implementation details of [`Zalsa`] and [`ZalsaLocal`].
use std::{marker::PhantomData, panic::RefUnwindSafe, sync::Arc};

use parking_lot::{Condvar, Mutex};
Expand All @@ -8,6 +9,55 @@ use crate::{
Database, Event, EventKind,
};

/// A handle to non-local database state.
pub struct StorageHandle<Db> {
// Note: Drop order is important, zalsa_impl needs to drop before coordinate
/// Reference to the database.
zalsa_impl: Arc<Zalsa>,

// Note: Drop order is important, coordinate needs to drop after zalsa_impl
/// Coordination data for cancellation of other handles when `zalsa_mut` is called.
/// This could be stored in Zalsa but it makes things marginally cleaner to keep it separate.
coordinate: CoordinateDrop,

/// We store references to `Db`
phantom: PhantomData<fn() -> Db>,
}

impl<Db> Clone for StorageHandle<Db> {
fn clone(&self) -> Self {
*self.coordinate.clones.lock() += 1;

Self {
zalsa_impl: self.zalsa_impl.clone(),
coordinate: CoordinateDrop(Arc::clone(&self.coordinate)),
phantom: PhantomData,
}
}
}

impl<Db: Database> Default for StorageHandle<Db> {
fn default() -> Self {
Self {
zalsa_impl: Arc::new(Zalsa::new::<Db>()),
coordinate: CoordinateDrop(Arc::new(Coordinate {
clones: Mutex::new(1),
cvar: Default::default(),
})),
phantom: PhantomData,
}
}
}

impl<Db> StorageHandle<Db> {
pub fn into_storage(self) -> Storage<Db> {
Storage {
handle: self,
zalsa_local: ZalsaLocal::new(),
}
}
}

/// Access the "storage" of a Salsa database: this is an internal plumbing trait
/// automatically implemented by `#[salsa::db]` applied to a struct.
///
Expand All @@ -20,76 +70,75 @@ pub unsafe trait HasStorage: Database + Clone + Sized {
fn storage_mut(&mut self) -> &mut Storage<Self>;
}

/// Concrete implementation of the [`Database`][] trait.
/// Takes an optional type parameter `U` that allows you to thread your own data.
pub struct Storage<Db: Database> {
// Note: Drop order is important, zalsa_impl needs to drop before coordinate
/// Reference to the database.
zalsa_impl: Arc<Zalsa>,

// Note: Drop order is important, coordinate needs to drop after zalsa_impl
/// Coordination data for cancellation of other handles when `zalsa_mut` is called.
/// This could be stored in Zalsa but it makes things marginally cleaner to keep it separate.
coordinate: CoordinateDrop,
/// Concrete implementation of the [`Database`] trait with local state that can be used to drive computations.
pub struct Storage<Db> {
handle: StorageHandle<Db>,

/// Per-thread state
zalsa_local: zalsa_local::ZalsaLocal,

/// We store references to `Db`
phantom: PhantomData<fn() -> Db>,
}

struct Coordinate {
/// Counter of the number of clones of actor. Begins at 1.
/// Incremented when cloned, decremented when dropped.
clones: Mutex<usize>,
cvar: Condvar,
}

// We cannot panic while holding a lock to `clones: Mutex<usize>` and therefore we cannot enter an
// inconsistent state.
impl RefUnwindSafe for Coordinate {}

impl<Db: Database> Default for Storage<Db> {
fn default() -> Self {
Self {
zalsa_impl: Arc::new(Zalsa::new::<Db>()),
coordinate: CoordinateDrop(Arc::new(Coordinate {
clones: Mutex::new(1),
cvar: Default::default(),
})),
handle: StorageHandle::default(),
zalsa_local: ZalsaLocal::new(),
phantom: PhantomData,
}
}
}

impl<Db: Database> Storage<Db> {
/// Discard the local state of this handle, turning it into a [`StorageHandle`] that is [`Sync`]
/// and [`std::panic::UnwindSafe`].
pub fn into_zalsa_handle(self) -> StorageHandle<Db> {
let Storage {
handle,
zalsa_local: _,
} = self;
handle
}

// ANCHOR: cancel_other_workers
/// 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_others(&self, db: &Db) {
self.zalsa_impl.set_cancellation_flag();
self.handle.zalsa_impl.set_cancellation_flag();

db.salsa_event(&|| Event::new(EventKind::DidSetCancellationFlag));

let mut clones = self.coordinate.clones.lock();
let mut clones = self.handle.coordinate.clones.lock();
while *clones != 1 {
self.coordinate.cvar.wait(&mut clones);
self.handle.coordinate.cvar.wait(&mut clones);
}
}
// ANCHOR_END: cancel_other_workers
}

unsafe impl<T: HasStorage> ZalsaDatabase for T {
fn zalsa(&self) -> &Zalsa {
&self.storage().zalsa_impl
&self.storage().handle.zalsa_impl
}

fn zalsa_mut(&mut self) -> &mut Zalsa {
self.storage().cancel_others(self);

let storage = self.storage_mut();
// The ref count on the `Arc` should now be 1
let zalsa_mut = Arc::get_mut(&mut storage.zalsa_impl).unwrap();
let zalsa_mut = Arc::get_mut(&mut storage.handle.zalsa_impl).unwrap();
zalsa_mut.new_revision();
zalsa_mut
}
Expand All @@ -103,17 +152,11 @@ unsafe impl<T: HasStorage> ZalsaDatabase for T {
}
}

impl<Db: Database> RefUnwindSafe for Storage<Db> {}

impl<Db: Database> Clone for Storage<Db> {
fn clone(&self) -> Self {
*self.coordinate.clones.lock() += 1;

Self {
zalsa_impl: self.zalsa_impl.clone(),
coordinate: CoordinateDrop(Arc::clone(&self.coordinate)),
handle: self.handle.clone(),
zalsa_local: ZalsaLocal::new(),
phantom: PhantomData,
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/zalsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use parking_lot::{Mutex, RwLock};
use rustc_hash::FxHashMap;
use std::any::{Any, TypeId};
use std::marker::PhantomData;
use std::panic::RefUnwindSafe;
use std::thread::ThreadId;

use crate::cycle::CycleRecoveryStrategy;
Expand Down Expand Up @@ -149,6 +150,13 @@ pub struct Zalsa {
runtime: Runtime,
}

// Our fields locked behind Mutices and RwLocks cannot enter an inconsistent state due to panics
// as they are all merely ID mappings with the exception of the `Runtime::dependency_graph`.
// `Runtime::dependency_graph` does not invoke user queries though and as such will not arbitrarily
// panic. The only way it may panic is by failing one of its asserts in which case we are already
// in a broken state anyways.
impl RefUnwindSafe for Zalsa {}

impl Zalsa {
pub(crate) fn new<Db: Database>() -> Self {
Self {
Expand Down
50 changes: 50 additions & 0 deletions tests/check_auto_traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Test that auto trait impls exist as expected.

use std::panic::UnwindSafe;

use salsa::Database;
use test_log::test;

#[salsa::input]
struct MyInput {
field: String,
}

#[salsa::tracked]
struct MyTracked<'db> {
field: MyInterned<'db>,
}

#[salsa::interned]
struct MyInterned<'db> {
field: String,
}

#[salsa::tracked]
fn test(db: &dyn Database, input: MyInput) {
let input = is_send(is_sync(input));
let interned = is_send(is_sync(MyInterned::new(db, input.field(db).clone())));
let _tracked_struct = is_send(is_sync(MyTracked::new(db, interned)));
}

fn is_send<T: Send>(t: T) -> T {
t
}

fn is_sync<T: Send>(t: T) -> T {
t
}

fn is_unwind_safe<T: UnwindSafe>(t: T) -> T {
t
}

#[test]
fn execute() {
let db = is_send(salsa::DatabaseImpl::new());
let _handle = is_send(is_sync(is_unwind_safe(
db.storage().clone().into_zalsa_handle(),
)));
let input = MyInput::new(&db, "Hello".to_string());
test(&db, input);
}
38 changes: 0 additions & 38 deletions tests/is_send_sync.rs

This file was deleted.