fix(peer): coalesce full UI view publications
Keep one queued and one replaceable pending snapshot for remote-library and Call-to-Play views while preserving lifecycle-event FIFO delivery. Generation barriers and fenced drains prevent stale nonempty views from crossing disable or acknowledgement boundaries. Test Plan: - just test - just clippy - focused burst, lifecycle ordering, fence, repeated-barrier, and stale-view tests - independent ordering review - git diff --check
This commit is contained in:
1 parent
470e119515
commit
37420e26ed
17 files changed
+828
-251
No files matched your search
@@ -12,13 +12,14 @@ use futures::{StreamExt as _, stream::FuturesUnordered};
|
||||
use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsBrowser, MdnsService, MdnsServicePoll};
|
||||
use lanspread_proto::{PROTOCOL_VERSION, PeerEndpoint, PeerId};
|
||||
use tokio::sync::{
|
||||
mpsc::{self, UnboundedSender},
|
||||
mpsc,
|
||||
oneshot,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
PeerEventSender,
|
||||
context::NetworkServiceCtx,
|
||||
events,
|
||||
services::{
|
||||
@@ -188,7 +189,7 @@ impl Drop for DiscoveryWorker {
|
||||
/// Runs the peer discovery service using mDNS.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn run_peer_discovery(
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: PeerEventSender,
|
||||
ctx: NetworkServiceCtx,
|
||||
) -> eyre::Result<()> {
|
||||
log::info!("Starting peer discovery task");
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use futures::{StreamExt as _, stream};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
PeerEventSender,
|
||||
config::{PEER_PING_IDLE_SECS, PEER_PING_INTERVAL_SECS, peer_stale_timeout},
|
||||
content_quarantine::ContentQuarantine,
|
||||
context::{NetworkServiceCtx, OperationKind},
|
||||
@@ -22,7 +20,7 @@ const MAX_CONCURRENT_PINGS: usize = 8;
|
||||
/// uses `last_revision_check`; inbound and content traffic only affect
|
||||
/// `last_seen`, which remains the stale-pruning clock.
|
||||
pub async fn run_ping_service(
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: PeerEventSender,
|
||||
ctx: NetworkServiceCtx,
|
||||
) -> eyre::Result<()> {
|
||||
log::info!(
|
||||
|
||||
@@ -14,13 +14,13 @@ use std::{
|
||||
|
||||
use futures::FutureExt;
|
||||
use tokio::{
|
||||
sync::{RwLock, mpsc::UnboundedSender},
|
||||
sync::RwLock,
|
||||
task::{JoinError, JoinSet},
|
||||
time::{Instant, MissedTickBehavior},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
PeerEventSender,
|
||||
config::{LOCAL_GAME_FALLBACK_SCAN_SECS, LOCAL_GAME_POLL_INTERVAL_SECS},
|
||||
context::Ctx,
|
||||
game_paths::{is_download_protected_root_name, is_ignored_games_root_name},
|
||||
@@ -67,10 +67,7 @@ struct RescanGate {
|
||||
}
|
||||
|
||||
/// Monitors the local game directory for changes.
|
||||
pub async fn run_local_game_monitor(
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
ctx: Ctx,
|
||||
) -> eyre::Result<()> {
|
||||
pub async fn run_local_game_monitor(tx_notify_ui: PeerEventSender, ctx: Ctx) -> eyre::Result<()> {
|
||||
log::info!("Starting polling-based local game directory monitor");
|
||||
|
||||
let mut snapshot = initial_poll_snapshot(&ctx).await;
|
||||
@@ -171,7 +168,7 @@ async fn initial_poll_snapshot(ctx: &Ctx) -> Option<PollSnapshot> {
|
||||
|
||||
async fn poll_local_game_changes(
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: &PeerEventSender,
|
||||
gate: &RescanGate,
|
||||
rescans: &mut JoinSet<()>,
|
||||
previous: &mut Option<PollSnapshot>,
|
||||
@@ -328,7 +325,7 @@ fn changed_game_ids(previous: &PollSnapshot, current: &PollSnapshot) -> BTreeSet
|
||||
|
||||
async fn queue_changed_games(
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: &PeerEventSender,
|
||||
gate: &RescanGate,
|
||||
rescans: &mut JoinSet<()>,
|
||||
changed_ids: BTreeSet<String>,
|
||||
@@ -354,7 +351,7 @@ async fn queue_changed_games(
|
||||
|
||||
async fn queue_rescan(
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: &PeerEventSender,
|
||||
gate: &RescanGate,
|
||||
rescans: &mut JoinSet<()>,
|
||||
id: String,
|
||||
@@ -376,12 +373,7 @@ async fn queue_rescan(
|
||||
});
|
||||
}
|
||||
|
||||
async fn run_gated_rescan(
|
||||
ctx: Ctx,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
gate: RescanGate,
|
||||
id: String,
|
||||
) {
|
||||
async fn run_gated_rescan(ctx: Ctx, tx_notify_ui: PeerEventSender, gate: RescanGate, id: String) {
|
||||
loop {
|
||||
gate.pending.write().await.remove(&id);
|
||||
|
||||
@@ -418,7 +410,7 @@ async fn run_gated_rescan(
|
||||
gate.running.write().await.remove(&id);
|
||||
}
|
||||
|
||||
async fn run_fallback_scan(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>) {
|
||||
async fn run_fallback_scan(ctx: &Ctx, tx_notify_ui: &PeerEventSender) {
|
||||
let _admission = ctx.operation_admission.lock().await;
|
||||
let game_dir = ctx.game_dir.read().await.clone();
|
||||
let catalog = ctx.catalog.catalog();
|
||||
@@ -445,11 +437,12 @@ mod tests {
|
||||
};
|
||||
|
||||
use lanspread_db::content_manifest::CatalogBundle;
|
||||
use tokio::sync::{RwLock, mpsc};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
UnpackFuture,
|
||||
Unpacker,
|
||||
context::OperationKind,
|
||||
@@ -512,9 +505,7 @@ mod tests {
|
||||
panic!("injected monitor loop panic");
|
||||
}
|
||||
|
||||
async fn recv_local_update(
|
||||
rx: &mut mpsc::UnboundedReceiver<PeerEvent>,
|
||||
) -> Vec<lanspread_db::db::Game> {
|
||||
async fn recv_local_update(rx: &mut crate::PeerEventReceiver) -> Vec<lanspread_db::db::Game> {
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("local update event should arrive")
|
||||
@@ -604,7 +595,7 @@ mod tests {
|
||||
.insert("game".to_string(), OperationKind::Downloading);
|
||||
let rescan_gate = RescanGate::default();
|
||||
let mut rescans = JoinSet::new();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let (tx, mut rx) = crate::peer_event_channel();
|
||||
|
||||
queue_changed_games(
|
||||
&ctx,
|
||||
@@ -635,7 +626,7 @@ mod tests {
|
||||
);
|
||||
let rescan_gate = RescanGate::default();
|
||||
let mut rescans = JoinSet::new();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let (tx, mut rx) = crate::peer_event_channel();
|
||||
let mut state = Some(
|
||||
capture_poll_snapshot(&ctx)
|
||||
.await
|
||||
@@ -669,7 +660,7 @@ mod tests {
|
||||
);
|
||||
let gate = RescanGate::default();
|
||||
let mut rescans = JoinSet::new();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let (tx, mut rx) = crate::peer_event_channel();
|
||||
|
||||
let library_guard = ctx.local_library.write().await;
|
||||
queue_rescan(&ctx, &tx, &gate, &mut rescans, "game".to_string()).await;
|
||||
@@ -707,7 +698,7 @@ mod tests {
|
||||
);
|
||||
let gate = RescanGate::default();
|
||||
let mut rescans = JoinSet::new();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let (tx, mut rx) = crate::peer_event_channel();
|
||||
let admission = ctx.operation_admission.lock().await;
|
||||
|
||||
queue_rescan(&ctx, &tx, &gate, &mut rescans, "game".to_string()).await;
|
||||
@@ -751,7 +742,7 @@ mod tests {
|
||||
);
|
||||
let gate = RescanGate::default();
|
||||
let mut rescans = JoinSet::new();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let (tx, mut rx) = crate::peer_event_channel();
|
||||
let admission = ctx.operation_admission.lock().await;
|
||||
|
||||
queue_rescan(&ctx, &tx, &gate, &mut rescans, "game".to_string()).await;
|
||||
@@ -792,7 +783,7 @@ mod tests {
|
||||
let temp = TempDir::new("lanspread-local-monitor-structured-shutdown");
|
||||
let ctx = test_ctx(temp.path().to_path_buf(), empty_catalog_bundle());
|
||||
let monitor_ctx = ctx.clone();
|
||||
let (tx, _rx) = mpsc::unbounded_channel();
|
||||
let (tx, _rx) = crate::peer_event_channel();
|
||||
let monitor = tokio::spawn(run_local_game_monitor(tx, monitor_ctx));
|
||||
|
||||
tokio::task::yield_now().await;
|
||||
@@ -813,7 +804,7 @@ mod tests {
|
||||
temp.path().to_path_buf(),
|
||||
catalog_bundle([("game", "20250101")]),
|
||||
);
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let (tx, mut rx) = crate::peer_event_channel();
|
||||
|
||||
run_fallback_scan(&ctx, &tx).await;
|
||||
|
||||
@@ -837,7 +828,7 @@ mod tests {
|
||||
temp.path().to_path_buf(),
|
||||
catalog_bundle([("game", "20250101")]),
|
||||
);
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let (tx, mut rx) = crate::peer_event_channel();
|
||||
|
||||
run_fallback_scan(&ctx, &tx).await;
|
||||
|
||||
|
||||
@@ -10,12 +10,13 @@ use lanspread_proto::{
|
||||
PeerStateSnapshot,
|
||||
RuntimeSessionId,
|
||||
};
|
||||
use tokio::sync::{RwLock, mpsc::UnboundedSender};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
CallToPlayView,
|
||||
PeerEvent,
|
||||
PeerEventSender,
|
||||
call_to_play::{
|
||||
CallToPlayPublication,
|
||||
CallToPlayStore,
|
||||
@@ -45,7 +46,7 @@ use crate::{
|
||||
pub(crate) struct RemoteStateCtx {
|
||||
local_peer_id: PeerId,
|
||||
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: PeerEventSender,
|
||||
call_to_play: Arc<RwLock<CallToPlayStore>>,
|
||||
quic: QuicConnector,
|
||||
cancellation: CancellationToken,
|
||||
@@ -56,7 +57,7 @@ impl RemoteStateCtx {
|
||||
#[must_use]
|
||||
pub(crate) fn from_network(
|
||||
ctx: &NetworkServiceCtx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: &PeerEventSender,
|
||||
) -> Self {
|
||||
Self {
|
||||
local_peer_id: ctx.peer_id,
|
||||
@@ -310,7 +311,7 @@ fn log_call_to_play_outcome(peer_id: PeerId, outcome: &ObserveRemoteAuthorOutcom
|
||||
fn enqueue_commit_transition(
|
||||
db: &PeerGameDB,
|
||||
call_to_play: &mut CallToPlayStore,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: &PeerEventSender,
|
||||
state_sync: &StateSyncHandle,
|
||||
publication: PreparedCallToPlayPublication,
|
||||
endpoint: PeerEndpoint,
|
||||
@@ -351,7 +352,7 @@ fn enqueue_commit_transition(
|
||||
fn enqueue_final_views(
|
||||
db: &PeerGameDB,
|
||||
call_to_play: &mut CallToPlayStore,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: &PeerEventSender,
|
||||
state_sync: &StateSyncHandle,
|
||||
publication: PreparedCallToPlayPublication,
|
||||
) {
|
||||
@@ -368,11 +369,11 @@ fn enqueue_final_views(
|
||||
}
|
||||
|
||||
/// Clears every remote projection after one network generation has fully
|
||||
/// drained, then queues the authoritative empty replacement views while the
|
||||
/// drained, then queues the authoritative replacement views while the
|
||||
/// database-to-Call-to-Play lock order is still held.
|
||||
pub(crate) async fn clear_remote_state_and_publish(
|
||||
ctx: &Ctx,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: &PeerEventSender,
|
||||
) {
|
||||
let mut db = ctx.peer_game_db.write().await;
|
||||
let mut call_to_play = ctx.call_to_play.write().await;
|
||||
@@ -383,11 +384,14 @@ pub(crate) async fn clear_remote_state_and_publish(
|
||||
events::send(tx_notify_ui, PeerEvent::PeerLost(endpoint));
|
||||
}
|
||||
events::send(tx_notify_ui, PeerEvent::PeerCountUpdated(0));
|
||||
events::send(
|
||||
tx_notify_ui,
|
||||
PeerEvent::RemoteLibraryView(events::remote_library_view(&db)),
|
||||
);
|
||||
enqueue_call_to_play_publication(publication, tx_notify_ui, &ctx.state_sync, true);
|
||||
ctx.state_sync
|
||||
.publish_call_to_play_revision(publication.local_revision);
|
||||
if let Err(error) = tx_notify_ui.publish_view_generation(
|
||||
events::remote_library_view(&db),
|
||||
CallToPlayView::from(publication.view),
|
||||
) {
|
||||
log::error!("Failed to publish cleared peer view generation: {error}");
|
||||
}
|
||||
|
||||
if let Some(error) = preparation_error {
|
||||
log::warn!(
|
||||
@@ -398,7 +402,7 @@ pub(crate) async fn clear_remote_state_and_publish(
|
||||
|
||||
fn enqueue_local_prune_if_changed(
|
||||
call_to_play: &mut CallToPlayStore,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: &PeerEventSender,
|
||||
state_sync: &StateSyncHandle,
|
||||
publication: PreparedCallToPlayPublication,
|
||||
) {
|
||||
@@ -411,7 +415,7 @@ fn enqueue_local_prune_if_changed(
|
||||
|
||||
fn enqueue_call_to_play_publication(
|
||||
publication: CallToPlayPublication,
|
||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: &PeerEventSender,
|
||||
state_sync: &StateSyncHandle,
|
||||
force_view: bool,
|
||||
) {
|
||||
|
||||
@@ -8,7 +8,7 @@ use tokio::sync::{Mutex, mpsc, watch};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
PeerEvent,
|
||||
PeerEventSender,
|
||||
context::NetworkServiceCtx,
|
||||
network::{send_call_to_play_changed, send_library_changed},
|
||||
peer_db::PeerRevisionSnapshot,
|
||||
@@ -170,7 +170,7 @@ type FanoutFuture = Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||
/// Runs the bounded pull scheduler and local hint fanout in one lexical scope.
|
||||
pub(crate) async fn run_state_sync(
|
||||
ctx: NetworkServiceCtx,
|
||||
tx_notify_ui: tokio::sync::mpsc::UnboundedSender<PeerEvent>,
|
||||
tx_notify_ui: PeerEventSender,
|
||||
cancellation: CancellationToken,
|
||||
) -> eyre::Result<()> {
|
||||
let mut pinned_rx = ctx.state_sync.inbox.pinned_rx.lock().await;
|
||||
|
||||
@@ -504,7 +504,7 @@ mod tests {
|
||||
CatalogContentManifestBody,
|
||||
CatalogExtractedEntry,
|
||||
};
|
||||
use tokio::sync::{RwLock, mpsc};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
use super::*;
|
||||
@@ -611,7 +611,7 @@ mod tests {
|
||||
root: &Path,
|
||||
manifest: &CatalogContentManifest,
|
||||
provider: Arc<CountingStreamInstallProvider>,
|
||||
) -> (PeerCtx, mpsc::UnboundedReceiver<crate::PeerEvent>) {
|
||||
) -> (PeerCtx, crate::PeerEventReceiver) {
|
||||
let catalog = Arc::new(
|
||||
CatalogBundle::from_manifests([manifest.clone()])
|
||||
.expect("test catalog should be complete"),
|
||||
@@ -643,7 +643,7 @@ mod tests {
|
||||
availability: Availability::Ready,
|
||||
},
|
||||
);
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let (tx, rx) = crate::peer_event_channel();
|
||||
(ctx.to_peer_ctx(tx, CancellationToken::new()), rx)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user