feat(qconnect): own remote-sync Mutex on QconnectApp via new(transport, sink, sync) + sync_handle (slice 2+4)
QconnectApp now holds the shared QconnectRemoteSyncState accumulator behind its own Mutex, disjoint from `state` (the two are never co-locked). The Tauri adapter still builds the Arc and passes the SAME clone to its event sink / service loop and into new(), so there is exactly one lock backing the session/liveness/renderer paths -- the app simply gains a handle to it via sync_handle(). Pure wiring, byte-identical locking, no logic moved. Prepares slices 2+4.4/5 to relocate the session-apply + watchdog logic into qconnect-app. cc
This commit is contained in:
2 files changed
+43
-4
No files matched your search
@@ -18,7 +18,10 @@ use serde_json::Value;
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{QconnectAppError, QconnectAppEvent, QconnectEventSink, QconnectRuntimeState};
|
use crate::{
|
||||||
|
QconnectAppError, QconnectAppEvent, QconnectEventSink, QconnectRemoteSyncState,
|
||||||
|
QconnectRuntimeState,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct QconnectApp<TTransport, TSink>
|
pub struct QconnectApp<TTransport, TSink>
|
||||||
where
|
where
|
||||||
@@ -28,6 +31,15 @@ where
|
|||||||
transport: Arc<TTransport>,
|
transport: Arc<TTransport>,
|
||||||
sink: Arc<TSink>,
|
sink: Arc<TSink>,
|
||||||
state: Arc<Mutex<QconnectRuntimeState>>,
|
state: Arc<Mutex<QconnectRuntimeState>>,
|
||||||
|
/// Cross-frontend remote-sync accumulator (session topology, renderer-state
|
||||||
|
/// cache, materialization cache, load-attempt dedup, watchdog epoch). Held
|
||||||
|
/// behind its OWN Mutex, disjoint from `state`: the two are never co-locked,
|
||||||
|
/// so the renderer-report hot path never serializes against session/watchdog
|
||||||
|
/// work and there is no deadlock edge against `clear_pending_if_matches`
|
||||||
|
/// (which locks `state`). The session loop (slice 5) and the renderer engine
|
||||||
|
/// (slice 6) both lock THIS one, exactly as they share it today via the Tauri
|
||||||
|
/// adapter. See `MASTER-qconnect-to-slint-plan.md` §0 (one Mutex, not two).
|
||||||
|
sync: Arc<Mutex<QconnectRemoteSyncState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<TTransport, TSink> Clone for QconnectApp<TTransport, TSink>
|
impl<TTransport, TSink> Clone for QconnectApp<TTransport, TSink>
|
||||||
@@ -40,6 +52,7 @@ where
|
|||||||
transport: Arc::clone(&self.transport),
|
transport: Arc::clone(&self.transport),
|
||||||
sink: Arc::clone(&self.sink),
|
sink: Arc::clone(&self.sink),
|
||||||
state: Arc::clone(&self.state),
|
state: Arc::clone(&self.state),
|
||||||
|
sync: Arc::clone(&self.sync),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,11 +64,20 @@ where
|
|||||||
{
|
{
|
||||||
const PENDING_ACTION_TIMEOUT_MS: u64 = 10_000;
|
const PENDING_ACTION_TIMEOUT_MS: u64 = 10_000;
|
||||||
|
|
||||||
pub fn new(transport: Arc<TTransport>, sink: Arc<TSink>) -> Self {
|
/// Construct the app. `sync` is the shared remote-sync accumulator handle:
|
||||||
|
/// the Tauri adapter builds it, hands a clone to its event sink / service
|
||||||
|
/// loop, and passes the SAME `Arc` here so there is exactly one lock backing
|
||||||
|
/// the session/liveness/renderer paths. The Slint adapter will do the same.
|
||||||
|
pub fn new(
|
||||||
|
transport: Arc<TTransport>,
|
||||||
|
sink: Arc<TSink>,
|
||||||
|
sync: Arc<Mutex<QconnectRemoteSyncState>>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
transport,
|
transport,
|
||||||
sink,
|
sink,
|
||||||
state: Arc::new(Mutex::new(QconnectRuntimeState::default())),
|
state: Arc::new(Mutex::new(QconnectRuntimeState::default())),
|
||||||
|
sync,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +85,13 @@ where
|
|||||||
Arc::clone(&self.state)
|
Arc::clone(&self.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shared handle to the remote-sync accumulator. The adapter uses this so its
|
||||||
|
/// event sink, service loop, and CoreBridge helpers lock the very same Mutex
|
||||||
|
/// the app's own session/watchdog methods lock.
|
||||||
|
pub fn sync_handle(&self) -> Arc<Mutex<QconnectRemoteSyncState>> {
|
||||||
|
Arc::clone(&self.sync)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn subscribe_transport_events(&self) -> tokio::sync::broadcast::Receiver<TransportEvent> {
|
pub fn subscribe_transport_events(&self) -> tokio::sync::broadcast::Receiver<TransportEvent> {
|
||||||
self.transport.subscribe()
|
self.transport.subscribe()
|
||||||
}
|
}
|
||||||
@@ -1006,6 +1035,8 @@ mod tests {
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use qconnect_core::QueueVersion;
|
use qconnect_core::QueueVersion;
|
||||||
use qconnect_transport_ws::InMemoryWsTransport;
|
use qconnect_transport_ws::InMemoryWsTransport;
|
||||||
|
|
||||||
|
use crate::QconnectRemoteSyncState;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
@@ -1053,7 +1084,11 @@ mod tests {
|
|||||||
) {
|
) {
|
||||||
let transport = Arc::new(InMemoryWsTransport::new());
|
let transport = Arc::new(InMemoryWsTransport::new());
|
||||||
let sink = TestSink::default();
|
let sink = TestSink::default();
|
||||||
let app = QconnectApp::new(Arc::clone(&transport), Arc::new(sink.clone()));
|
let app = QconnectApp::new(
|
||||||
|
Arc::clone(&transport),
|
||||||
|
Arc::new(sink.clone()),
|
||||||
|
Arc::new(Mutex::new(QconnectRemoteSyncState::default())),
|
||||||
|
);
|
||||||
let events_rx = app.subscribe_transport_events();
|
let events_rx = app.subscribe_transport_events();
|
||||||
app.connect(test_config()).await.expect("connect");
|
app.connect(test_config()).await.expect("connect");
|
||||||
(app, sink, transport, events_rx)
|
(app, sink, transport, events_rx)
|
||||||
|
|||||||
@@ -304,7 +304,11 @@ impl QconnectServiceState {
|
|||||||
sync_state: Arc::clone(&sync_state),
|
sync_state: Arc::clone(&sync_state),
|
||||||
app: Arc::new(std::sync::OnceLock::new()),
|
app: Arc::new(std::sync::OnceLock::new()),
|
||||||
});
|
});
|
||||||
let app = Arc::new(QconnectApp::new(transport, Arc::clone(&sink)));
|
let app = Arc::new(QconnectApp::new(
|
||||||
|
transport,
|
||||||
|
Arc::clone(&sink),
|
||||||
|
Arc::clone(&sync_state),
|
||||||
|
));
|
||||||
// P1-6: wire the owning app into the sink so it can emit reports
|
// P1-6: wire the owning app into the sink so it can emit reports
|
||||||
// (e.g. is_active=true after SetActive(true)).
|
// (e.g. is_active=true after SetActive(true)).
|
||||||
sink.set_app(&app);
|
sink.set_app(&app);
|
||||||
|
|||||||
Reference in new issue
Block a user