Skip to content
Merged
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
13 changes: 9 additions & 4 deletions aimdb-sync/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
publish path is one relaxed atomic load, and a program that never attaches
never installs a handler. A database the child attaches *itself* after
forking is unaffected — the guard is a generation counter, not a poison flag.
The detection is entirely internal: a facade built on this crate will have the
same problem for the same reason, but none exists yet, so exposing the
stamp-and-compare pair would commit the crate in semver to a model chosen
against no real caller.
The detection itself stays internal: a facade asks `SyncProducer::check()`
rather than the generation counter, so the semver commitment is to the
question and not to the mechanism.
- **A panic-freedom contract on the blocking surface.** The crate is compiled
under `deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)` outside
its own tests, so "a panic here is a bug, not an error channel" is checked
Expand All @@ -100,6 +99,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
process dying. Documented with its two limits: `block_on` still panics if
called from inside a Tokio runtime, and the guarantee stops at this crate's
edge.
- **`SyncProducer::check()`.** Answers "can a publish through this producer
still reach the database?" without publishing — `Ok(())`, or
`RuntimeShutdown` / `ForkedChild`. It is the check `set()` already performs,
exposed rather than duplicated, so the answer cannot drift from what a publish
would find. It takes no lock, which is what makes it usable from a facade
whose own teardown holds the lock its `AimDbHandle` sits behind.
- **`SyncError::kind()`.** Returns `aimdb_core::DbErrorKind` rather than a kind
of its own, so a caller — an FFI layer above all — has one set of actions for
the whole stack instead of one per crate. The `Db` arm delegates, so a buffer
Expand Down
10 changes: 5 additions & 5 deletions aimdb-sync/src/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ fn load() -> Generation {

/// The generation to stamp on something being created now.
///
/// Crate-private for now. A layer built *on* this crate has the same problem —
/// an FFI door holds state of its own that a `fork` invalidates — but no such
/// layer exists yet, and exposing this would commit us in semver to the
/// stamp-and-compare model. Widen it when something real needs it, so the
/// shape can be chosen against that caller rather than guessed at.
/// Crate-private, and staying that way. A layer built *on* this crate has the
/// same problem, and is served by
/// [`SyncProducer::check`](crate::SyncProducer::check) — the question it
/// actually has ("can I still publish?"), rather than the stamp-and-compare
/// mechanism, which publishing this pair would pin us to in semver.
///
/// **Arms the handler**, because otherwise it hands out a number that cannot
/// change. A caller above this crate stamps its own state before any database
Expand Down
15 changes: 15 additions & 0 deletions aimdb-sync/src/producer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ where
self.rt.upgrade().ok_or(SyncError::RuntimeShutdown)
}

/// Whether a publish through this producer can still reach the database.
///
/// `Ok(())` means [`set`](Self::set) reaches the record graph — not that it
/// succeeds; an unregistered key still fails there. Otherwise
/// [`SyncError::RuntimeShutdown`] or [`SyncError::ForkedChild`].
///
/// This is the check [`set`](Self::set) performs, not a second one beside
/// it, so the answer cannot drift from what a publish would find. It takes
/// no lock — a [`Weak`] upgrade and one relaxed atomic load — so a facade
/// can ask it while its own teardown holds the handle's lock.
#[inline]
pub fn check(&self) -> SyncResult<()> {
self.runtime()?.check()
}

/// Set the value, blocking until it can be sent.
///
/// This call will block the current thread until the value can be sent to the runtime thread.
Expand Down
5 changes: 4 additions & 1 deletion aimdb-sync/tests/fork_safety_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ fn a_forked_child_is_refused_rather_than_silently_dropped() {
inherited.try_set(Reading { value: 2 }),
Err(SyncError::ForkedChild)
);
// The same refusal, asked rather than provoked: `check()` has to agree
// with the two publishes above.
let check_agrees = matches!(inherited.check(), Err(SyncError::ForkedChild));
// Leak rather than free. The child is about to `_exit`, which reclaims
// everything anyway, and `free` is the unsafe act here: it takes the
// allocator lock, which a thread that did not survive the fork may have
Expand All @@ -64,7 +67,7 @@ fn a_forked_child_is_refused_rather_than_silently_dropped() {
// the destructor, so there is nothing to lose by not running it.
std::mem::forget(inherited);

refused && refused_try
refused && refused_try && check_agrees
});
assert_eq!(code, 0, "the child's publishes should have been refused");

Expand Down
17 changes: 17 additions & 0 deletions aimdb-sync/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,23 @@ fn test_runtime_shutdown_error() {
assert!(matches!(result, Err(SyncError::RuntimeShutdown)));
}

/// `check()` answers the same question `set()` does, without publishing.
#[test]
fn check_reports_what_a_publish_would_find() {
let (handle, producer, _consumer) = setup(BufferCfg::SpmcRing { capacity: 10 });

producer.check().expect("usable while attached");

handle.detach().expect("Failed to detach");

// The same verdict `set()` reaches, arrived at without sending anything.
assert!(matches!(producer.check(), Err(SyncError::RuntimeShutdown)));
assert!(matches!(
producer.set(test_value()),
Err(SyncError::RuntimeShutdown)
));
}

/// Test error handling - runtime shutdown, non-blocking operations
#[test]
fn test_runtime_shutdown_error_non_blocking() {
Expand Down