Skip to content

Commit 4fb165b

Browse files
maltesanderclaude
andcommitted
test: model a cancel racing the execution that replaces its token
Minting per execution made this interleaving reachable for the first time: set_cancel used to run at most once per statement, so nothing could race a replacement. Now every statement-producing call writes while sql_cancel reads with no group lock, serialised only by the registry's own RwLock. Which of the two tokens the canceller observes is deliberately not asserted -- both are correct, and the spec says so. What the model earns is that neither thread panics, the downcast always succeeds, and the read never yields None for a slot that has held a token throughout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ef07d52 commit 4fb165b

1 file changed

Lines changed: 55 additions & 0 deletions

File tree

src/handles/registry.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,61 @@ mod loom_tests {
868868
});
869869
}
870870

871+
/// A `SQLCancel` reading a statement's token while a new execution is
872+
/// replacing it must observe one whole token or the other, never a torn or
873+
/// absent one.
874+
///
875+
/// This interleaving only became reachable when cancel tokens started being
876+
/// minted per execution rather than once per statement: `set_cancel` used
877+
/// to run at most once, so nothing could race a *replacement*. Now every
878+
/// statement-producing call writes, while `sql_cancel` reads with no group
879+
/// lock at all — the two are serialised only by the registry's own
880+
/// `RwLock`.
881+
///
882+
/// *Which* of the two tokens the canceller gets is deliberately not
883+
/// asserted: both are correct. Getting the outgoing one means cancelling an
884+
/// execution that has already finished, which the spec defines as a no-op
885+
/// ("a call to SQLCancel when no processing is being done on the statement
886+
/// ... has is [sic] no effect at all"), and getting the incoming one means
887+
/// cancelling the run that is actually in flight. What the model earns is
888+
/// that neither thread panics, the downcast always succeeds, and the read
889+
/// never yields `None` for a slot that has held a token throughout.
890+
#[test]
891+
fn a_cancel_token_replaced_by_a_new_execution_is_never_torn() {
892+
loom::model(|| {
893+
let reg = Arc::new(Registry::new());
894+
let (stmt, _, _) = reg
895+
.register(HandleKind::Stmt, 0x1000, GroupLock::new(), None)
896+
.expect("registered");
897+
// The token the first execution minted.
898+
reg.set_cancel(stmt, StdArc::new(1u32) as StdArc<dyn Any + Send + Sync>);
899+
900+
let executor = {
901+
let reg = Arc::clone(&reg);
902+
thread::spawn(move || {
903+
// A second execution on the same statement mints its own.
904+
reg.set_cancel(stmt, StdArc::new(2u32) as StdArc<dyn Any + Send + Sync>);
905+
})
906+
};
907+
let canceller = {
908+
let reg = Arc::clone(&reg);
909+
thread::spawn(move || {
910+
let token = reg
911+
.cancel_of(stmt)
912+
.expect("a token has been set since before either thread started");
913+
let seen = *token.downcast_ref::<u32>().expect("type");
914+
assert!(
915+
seen == 1 || seen == 2,
916+
"observed a token that was never set"
917+
);
918+
})
919+
};
920+
921+
executor.join().expect("executor panicked");
922+
canceller.join().expect("canceller panicked");
923+
});
924+
}
925+
871926
/// `SQLEndTran(SQL_HANDLE_ENV)` walking a connection that is freed
872927
/// underneath it must skip that connection, not report failure.
873928
///

0 commit comments

Comments
 (0)