fix(core): linearize session publication
This commit is contained in:
+114
-12
@@ -235,6 +235,13 @@ struct SessionState {
|
|||||||
features: Vec<String>,
|
features: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum SessionPublication {
|
||||||
|
Starting,
|
||||||
|
Published,
|
||||||
|
Failed(CoreError),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||||
enum Lifecycle {
|
enum Lifecycle {
|
||||||
Created,
|
Created,
|
||||||
@@ -895,7 +902,10 @@ unsafe extern "C" fn verse_core_connect_v1(
|
|||||||
bytes,
|
bytes,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let runtime = match tokio::runtime::Runtime::new() {
|
let runtime = match tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
{
|
||||||
Ok(runtime) => runtime,
|
Ok(runtime) => runtime,
|
||||||
Err(_) => return INTERNAL,
|
Err(_) => return INTERNAL,
|
||||||
};
|
};
|
||||||
@@ -905,6 +915,8 @@ unsafe extern "C" fn verse_core_connect_v1(
|
|||||||
};
|
};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
let test_session = credential.certificate_chain_pem().contains("AQID");
|
let test_session = credential.certificate_chain_pem().contains("AQID");
|
||||||
|
#[cfg(test)]
|
||||||
|
let test_session_failure = credential.certificate_chain_pem().contains("AQIDFAIL");
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
let test_session = false;
|
let test_session = false;
|
||||||
let session = if test_session {
|
let session = if test_session {
|
||||||
@@ -951,7 +963,10 @@ unsafe extern "C" fn verse_core_connect_v1(
|
|||||||
let features = manifest.features().to_vec();
|
let features = manifest.features().to_vec();
|
||||||
let (command_tx, command_rx) = tokio::sync::mpsc::channel(CONTROL_QUEUE_CAPACITY);
|
let (command_tx, command_rx) = tokio::sync::mpsc::channel(CONTROL_QUEUE_CAPACITY);
|
||||||
let (session_done_tx, session_done_rx) = mpsc::channel();
|
let (session_done_tx, session_done_rx) = mpsc::channel();
|
||||||
|
let (session_started_tx, session_started_rx) = mpsc::channel();
|
||||||
let (event_tx, event_rx) = bounded_session_events();
|
let (event_tx, event_rx) = bounded_session_events();
|
||||||
|
let publication = Arc::new(Mutex::new(SessionPublication::Starting));
|
||||||
|
let worker_publication = Arc::clone(&publication);
|
||||||
let callbacks = handle.callbacks;
|
let callbacks = handle.callbacks;
|
||||||
let id = handle.id;
|
let id = handle.id;
|
||||||
let callbacks_closed = Arc::clone(&handle.callbacks_closed);
|
let callbacks_closed = Arc::clone(&handle.callbacks_closed);
|
||||||
@@ -1000,9 +1015,25 @@ unsafe extern "C" fn verse_core_connect_v1(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let session_result = if let Some(session) = session {
|
let session_result = if let Some(session) = session {
|
||||||
runtime.block_on(session.run(command_rx, event_tx, cancellation))
|
runtime.block_on(session.run_with_startup(
|
||||||
|
command_rx,
|
||||||
|
event_tx,
|
||||||
|
cancellation,
|
||||||
|
Some(session_started_tx),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
event_tx.close();
|
event_tx.close();
|
||||||
|
#[cfg(test)]
|
||||||
|
if test_session_failure {
|
||||||
|
let mut publication = lock(&worker_publication);
|
||||||
|
*publication = SessionPublication::Failed(CoreError::Transport);
|
||||||
|
let _ = session_started_tx.send(());
|
||||||
|
drop(publication);
|
||||||
|
let _ = bridge.join();
|
||||||
|
let _ = session_done_tx.send(());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _ = session_started_tx.send(());
|
||||||
runtime.block_on(async move {
|
runtime.block_on(async move {
|
||||||
let mut command_rx = command_rx;
|
let mut command_rx = command_rx;
|
||||||
while !cancellation.is_cancelled() {
|
while !cancellation.is_cancelled() {
|
||||||
@@ -1019,17 +1050,30 @@ unsafe extern "C" fn verse_core_connect_v1(
|
|||||||
};
|
};
|
||||||
let _ = bridge.join();
|
let _ = bridge.join();
|
||||||
if let Err(error) = session_result {
|
if let Err(error) = session_result {
|
||||||
let mut state = lock(&session_handle.state);
|
let published = {
|
||||||
if state.lifecycle == Lifecycle::Connected
|
let mut publication = lock(&worker_publication);
|
||||||
&& !session_handle.callbacks_closed.load(Ordering::Acquire)
|
match *publication {
|
||||||
{
|
SessionPublication::Starting => {
|
||||||
if error != CoreError::Cancelled {
|
*publication = SessionPublication::Failed(error);
|
||||||
enqueue_callback_under_state_lock(
|
false
|
||||||
&session_handle,
|
}
|
||||||
CallbackEvent::Error(status(error)),
|
SessionPublication::Published => true,
|
||||||
);
|
SessionPublication::Failed(_) => false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if published {
|
||||||
|
let mut state = lock(&session_handle.state);
|
||||||
|
if state.lifecycle == Lifecycle::Connected
|
||||||
|
&& !session_handle.callbacks_closed.load(Ordering::Acquire)
|
||||||
|
{
|
||||||
|
if error != CoreError::Cancelled {
|
||||||
|
enqueue_callback_under_state_lock(
|
||||||
|
&session_handle,
|
||||||
|
CallbackEvent::Error(status(error)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
apply_cancellation(&session_handle, &mut state);
|
||||||
}
|
}
|
||||||
apply_cancellation(&session_handle, &mut state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = session_done_tx.send(());
|
let _ = session_done_tx.send(());
|
||||||
@@ -1040,8 +1084,26 @@ unsafe extern "C" fn verse_core_connect_v1(
|
|||||||
};
|
};
|
||||||
*lock(&handle.session_done) = Some(session_done_rx);
|
*lock(&handle.session_done) = Some(session_done_rx);
|
||||||
*lock(&handle.session_worker) = Some(session_worker);
|
*lock(&handle.session_worker) = Some(session_worker);
|
||||||
|
if session_started_rx
|
||||||
|
.recv_timeout(Duration::from_secs(10))
|
||||||
|
.is_err()
|
||||||
{
|
{
|
||||||
|
cancel_inner(&handle);
|
||||||
|
return INTERNAL;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut publication = lock(&publication);
|
||||||
let mut state = lock(&handle.state);
|
let mut state = lock(&handle.state);
|
||||||
|
if let SessionPublication::Failed(error) = *publication {
|
||||||
|
handle
|
||||||
|
.transition
|
||||||
|
.store(TRANSITION_CANCELLED, Ordering::Release);
|
||||||
|
if error != CoreError::Cancelled {
|
||||||
|
enqueue_callback_under_state_lock(&handle, CallbackEvent::Error(status(error)));
|
||||||
|
}
|
||||||
|
apply_cancellation(&handle, &mut state);
|
||||||
|
return status(error);
|
||||||
|
}
|
||||||
if cancellation_admitted(&handle) || state.lifecycle == Lifecycle::Destroying {
|
if cancellation_admitted(&handle) || state.lifecycle == Lifecycle::Destroying {
|
||||||
apply_cancellation(&handle, &mut state);
|
apply_cancellation(&handle, &mut state);
|
||||||
return CANCELLED;
|
return CANCELLED;
|
||||||
@@ -1061,6 +1123,7 @@ unsafe extern "C" fn verse_core_connect_v1(
|
|||||||
state.command_tx = Some(command_tx);
|
state.command_tx = Some(command_tx);
|
||||||
state.features = features;
|
state.features = features;
|
||||||
state.lifecycle = Lifecycle::Connected;
|
state.lifecycle = Lifecycle::Connected;
|
||||||
|
*publication = SessionPublication::Published;
|
||||||
enqueue_callback_under_state_lock(&handle, CallbackEvent::State(STATE_CONNECTED));
|
enqueue_callback_under_state_lock(&handle, CallbackEvent::State(STATE_CONNECTED));
|
||||||
if cancellation_admitted(&handle) {
|
if cancellation_admitted(&handle) {
|
||||||
apply_cancellation(&handle, &mut state);
|
apply_cancellation(&handle, &mut state);
|
||||||
@@ -1364,6 +1427,7 @@ mod tests {
|
|||||||
ConnectTransitionHook, CoreHandle, FinalConnectHook, Lifecycle, SignFn, StateEvent,
|
ConnectTransitionHook, CoreHandle, FinalConnectHook, Lifecycle, SignFn, StateEvent,
|
||||||
StateFn, BUSY, CANCELLED, CANCEL_ADMISSION_HOOK, FINAL_CONNECT_HOOK, FINAL_PUBLISH_HOOK,
|
StateFn, BUSY, CANCELLED, CANCEL_ADMISSION_HOOK, FINAL_CONNECT_HOOK, FINAL_PUBLISH_HOOK,
|
||||||
INITIAL_CONNECT_HOOK, INTERNAL, OK, STATE_CANCELLED, STATE_CONNECTED, STATE_CONNECTING,
|
INITIAL_CONNECT_HOOK, INTERNAL, OK, STATE_CANCELLED, STATE_CONNECTED, STATE_CONNECTING,
|
||||||
|
TRANSPORT,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MANIFEST: &[u8] = br#"{
|
const MANIFEST: &[u8] = br#"{
|
||||||
@@ -1684,4 +1748,42 @@ mod tests {
|
|||||||
[STATE_CONNECTING, STATE_CANCELLED]
|
[STATE_CONNECTING, STATE_CANCELLED]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn terminal_session_failure_before_publication_never_connects() {
|
||||||
|
let context = Box::new(CallbackContext::default());
|
||||||
|
let context_pointer = ptr::from_ref(&*context).cast_mut().cast::<c_void>();
|
||||||
|
let core = create_core_with_callback(context_pointer, Some(record_state));
|
||||||
|
let failing_credential = String::from_utf8(CREDENTIAL.to_vec())
|
||||||
|
.expect("credential UTF-8")
|
||||||
|
.replace("AQID", "AQIDFAIL");
|
||||||
|
let request = ConnectRequest {
|
||||||
|
struct_size: u32::try_from(size_of::<ConnectRequest>()).expect("request size"),
|
||||||
|
abi_version: 1,
|
||||||
|
manifest_json: BytesView {
|
||||||
|
data: MANIFEST.as_ptr(),
|
||||||
|
length: MANIFEST.len(),
|
||||||
|
},
|
||||||
|
tunnel_credential_json: BytesView {
|
||||||
|
data: failing_credential.as_ptr(),
|
||||||
|
length: failing_credential.len(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { super::verse_core_connect_v1(core, &raw const request) },
|
||||||
|
TRANSPORT
|
||||||
|
);
|
||||||
|
wait_for_state(&context, STATE_CANCELLED);
|
||||||
|
assert_eq!(
|
||||||
|
unsafe { super::verse_core_request_idr_v1(core) },
|
||||||
|
CANCELLED,
|
||||||
|
"terminal connect failure must commit the cancelled transition"
|
||||||
|
);
|
||||||
|
assert_eq!(unsafe { super::verse_core_destroy_v1(core, 2_000) }, OK);
|
||||||
|
assert_eq!(
|
||||||
|
context.states.lock().expect("states lock").as_slice(),
|
||||||
|
[STATE_CONNECTING, STATE_CANCELLED]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,6 +331,17 @@ impl TransportSession {
|
|||||||
commands: tokio::sync::mpsc::Receiver<SessionCommand>,
|
commands: tokio::sync::mpsc::Receiver<SessionCommand>,
|
||||||
events: SessionEventSender,
|
events: SessionEventSender,
|
||||||
cancellation: Cancellation,
|
cancellation: Cancellation,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.run_with_startup(commands, events, cancellation, None)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn run_with_startup(
|
||||||
|
self,
|
||||||
|
commands: tokio::sync::mpsc::Receiver<SessionCommand>,
|
||||||
|
events: SessionEventSender,
|
||||||
|
cancellation: Cancellation,
|
||||||
|
started: Option<std::sync::mpsc::Sender<()>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let Self {
|
let Self {
|
||||||
endpoint,
|
endpoint,
|
||||||
@@ -358,6 +369,9 @@ impl TransportSession {
|
|||||||
result_tx.clone(),
|
result_tx.clone(),
|
||||||
));
|
));
|
||||||
let datagrams = tokio::spawn(datagram_loop(connection.clone(), events.clone(), result_tx));
|
let datagrams = tokio::spawn(datagram_loop(connection.clone(), events.clone(), result_tx));
|
||||||
|
if let Some(started) = started {
|
||||||
|
let _ = started.send(());
|
||||||
|
}
|
||||||
loop {
|
loop {
|
||||||
if cancellation.check().is_err() {
|
if cancellation.check().is_err() {
|
||||||
connection.close(APPLICATION_ERROR, b"cancelled");
|
connection.close(APPLICATION_ERROR, b"cancelled");
|
||||||
|
|||||||
@@ -742,6 +742,7 @@ fn signer_callback_rejects_every_stateful_api_across_handles() {
|
|||||||
let other = create(&mut other_ctx);
|
let other = create(&mut other_ctx);
|
||||||
|
|
||||||
let mut ctx = Context::default();
|
let mut ctx = Context::default();
|
||||||
|
ctx.reentry_cancel.store(OK, Ordering::SeqCst);
|
||||||
ctx.reentry_target.store(other as usize, Ordering::SeqCst);
|
ctx.reentry_target.store(other as usize, Ordering::SeqCst);
|
||||||
let mut cfg = config(&mut ctx);
|
let mut cfg = config(&mut ctx);
|
||||||
cfg.sign_admission = Some(sign_admission_probes_global_reentry);
|
cfg.sign_admission = Some(sign_admission_probes_global_reentry);
|
||||||
|
|||||||
@@ -204,25 +204,52 @@ pub(crate) struct Ready {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Oracle {
|
pub(crate) struct Oracle {
|
||||||
_serial: std::sync::MutexGuard<'static, ()>,
|
|
||||||
child: Child,
|
child: Child,
|
||||||
directory: std::path::PathBuf,
|
directory: std::path::PathBuf,
|
||||||
pub(crate) ready: Ready,
|
pub(crate) ready: Ready,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn production_oracles_can_run_concurrently_with_isolated_state() {
|
||||||
|
let (first_ready_tx, first_ready_rx) = std::sync::mpsc::channel();
|
||||||
|
let (release_tx, release_rx) = std::sync::mpsc::channel();
|
||||||
|
let first = thread::spawn(move || {
|
||||||
|
let oracle = Oracle::start("");
|
||||||
|
first_ready_tx.send(()).expect("first ready");
|
||||||
|
release_rx.recv().expect("release first oracle");
|
||||||
|
drop(oracle);
|
||||||
|
});
|
||||||
|
first_ready_rx
|
||||||
|
.recv_timeout(Duration::from_secs(20))
|
||||||
|
.expect("first oracle ready");
|
||||||
|
let (second_ready_tx, second_ready_rx) = std::sync::mpsc::channel();
|
||||||
|
let second = thread::spawn(move || {
|
||||||
|
let oracle = Oracle::start("");
|
||||||
|
second_ready_tx.send(()).expect("second ready");
|
||||||
|
drop(oracle);
|
||||||
|
});
|
||||||
|
let concurrent = second_ready_rx.recv_timeout(Duration::from_secs(5)).is_ok();
|
||||||
|
release_tx.send(()).expect("release first");
|
||||||
|
first.join().expect("first oracle");
|
||||||
|
second.join().expect("second oracle");
|
||||||
|
assert!(concurrent, "second isolated oracle was globally serialized");
|
||||||
|
}
|
||||||
|
|
||||||
impl Oracle {
|
impl Oracle {
|
||||||
pub(crate) fn start(mode: &str) -> Self {
|
pub(crate) fn start(mode: &str) -> Self {
|
||||||
static SERIAL: Mutex<()> = Mutex::new(());
|
|
||||||
static NEXT: AtomicUsize = AtomicUsize::new(1);
|
static NEXT: AtomicUsize = AtomicUsize::new(1);
|
||||||
let serial = SERIAL
|
let directory = loop {
|
||||||
.lock()
|
let candidate = std::env::temp_dir().join(format!(
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
"versevdi-rust-gateway-oracle-{}-{}",
|
||||||
let directory = std::env::temp_dir().join(format!(
|
std::process::id(),
|
||||||
"versevdi-rust-gateway-oracle-{}-{}",
|
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||||
std::process::id(),
|
));
|
||||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
match fs::create_dir(&candidate) {
|
||||||
));
|
Ok(()) => break candidate,
|
||||||
fs::create_dir(&directory).expect("create oracle directory");
|
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||||
|
Err(error) => panic!("create oracle directory: {error}"),
|
||||||
|
}
|
||||||
|
};
|
||||||
let source = directory.join("main.go");
|
let source = directory.join("main.go");
|
||||||
let ready_path = directory.join("ready.json");
|
let ready_path = directory.join("ready.json");
|
||||||
let stop_path = directory.join("stop");
|
let stop_path = directory.join("stop");
|
||||||
@@ -242,7 +269,6 @@ impl Oracle {
|
|||||||
if let Ok(bytes) = fs::read(&ready_path) {
|
if let Ok(bytes) = fs::read(&ready_path) {
|
||||||
if let Ok(ready) = serde_json::from_slice(&bytes) {
|
if let Ok(ready) = serde_json::from_slice(&bytes) {
|
||||||
return Self {
|
return Self {
|
||||||
_serial: serial,
|
|
||||||
child,
|
child,
|
||||||
directory,
|
directory,
|
||||||
ready,
|
ready,
|
||||||
@@ -522,6 +548,8 @@ unsafe extern "C" {
|
|||||||
struct AbiSigners {
|
struct AbiSigners {
|
||||||
admission: Arc<dyn SigningKey>,
|
admission: Arc<dyn SigningKey>,
|
||||||
tls: Arc<dyn SigningKey>,
|
tls: Arc<dyn SigningKey>,
|
||||||
|
admission_threads: Mutex<Vec<thread::ThreadId>>,
|
||||||
|
tls_threads: Mutex<Vec<thread::ThreadId>>,
|
||||||
}
|
}
|
||||||
unsafe extern "C" fn abi_admission(raw: *mut c_void, input: AbiBytes, output: *mut u8) -> u32 {
|
unsafe extern "C" fn abi_admission(raw: *mut c_void, input: AbiBytes, output: *mut u8) -> u32 {
|
||||||
unsafe { abi_sign(raw, input, output, true) }
|
unsafe { abi_sign(raw, input, output, true) }
|
||||||
@@ -531,6 +559,15 @@ unsafe extern "C" fn abi_tls(raw: *mut c_void, input: AbiBytes, output: *mut u8)
|
|||||||
}
|
}
|
||||||
unsafe fn abi_sign(raw: *mut c_void, input: AbiBytes, output: *mut u8, admission: bool) -> u32 {
|
unsafe fn abi_sign(raw: *mut c_void, input: AbiBytes, output: *mut u8, admission: bool) -> u32 {
|
||||||
let context = unsafe { &*raw.cast::<AbiSigners>() };
|
let context = unsafe { &*raw.cast::<AbiSigners>() };
|
||||||
|
let threads = if admission {
|
||||||
|
&context.admission_threads
|
||||||
|
} else {
|
||||||
|
&context.tls_threads
|
||||||
|
};
|
||||||
|
threads
|
||||||
|
.lock()
|
||||||
|
.expect("signer threads")
|
||||||
|
.push(thread::current().id());
|
||||||
let bytes = unsafe { std::slice::from_raw_parts(input.data, input.length) };
|
let bytes = unsafe { std::slice::from_raw_parts(input.data, input.length) };
|
||||||
let key = if admission {
|
let key = if admission {
|
||||||
&context.admission
|
&context.admission
|
||||||
@@ -552,6 +589,8 @@ fn real_abi_exports_connect_send_cancel_and_destroy_the_go_gateway_session() {
|
|||||||
let mut signers = Box::new(AbiSigners {
|
let mut signers = Box::new(AbiSigners {
|
||||||
admission: test_key(&oracle.ready.admission_key),
|
admission: test_key(&oracle.ready.admission_key),
|
||||||
tls: test_key(&oracle.ready.client_key),
|
tls: test_key(&oracle.ready.client_key),
|
||||||
|
admission_threads: Mutex::new(Vec::new()),
|
||||||
|
tls_threads: Mutex::new(Vec::new()),
|
||||||
});
|
});
|
||||||
let config = AbiConfig {
|
let config = AbiConfig {
|
||||||
struct_size: size_of::<AbiConfig>() as u32,
|
struct_size: size_of::<AbiConfig>() as u32,
|
||||||
@@ -579,7 +618,20 @@ fn real_abi_exports_connect_send_cancel_and_destroy_the_go_gateway_session() {
|
|||||||
length: oracle.ready.credential.len(),
|
length: oracle.ready.credential.len(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
let calling_thread = thread::current().id();
|
||||||
assert_eq!(unsafe { verse_core_connect_v1(core, &request) }, 0);
|
assert_eq!(unsafe { verse_core_connect_v1(core, &request) }, 0);
|
||||||
|
assert_eq!(
|
||||||
|
signers
|
||||||
|
.admission_threads
|
||||||
|
.lock()
|
||||||
|
.expect("admission threads")
|
||||||
|
.as_slice(),
|
||||||
|
[calling_thread]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
signers.tls_threads.lock().expect("TLS threads").as_slice(),
|
||||||
|
[calling_thread]
|
||||||
|
);
|
||||||
let mut values = [0; 12];
|
let mut values = [0; 12];
|
||||||
values[0] = 1;
|
values[0] = 1;
|
||||||
values[2] = 30;
|
values[2] = 30;
|
||||||
|
|||||||
Reference in New Issue
Block a user