d149143535
Step 3 (F47 buffered unsubscribe skip):
- crates/mxaccess-compat/tests/buffered_unsubscribe_skip_live.rs.
- Subscribe buffered, sleep so the engine has DataUpdates in flight,
then call unsubscribe. Asserts Ok return without surfacing transport
or HRESULT errors.
- Session::unsubscribe (session.rs:2261) probes the registry: if
Buffered { .. }, it skips nmx.un_advise entirely, mirroring the .NET
reference's `if (!subscription.IsBuffered)` guard at
MxNativeSession.cs:361-381. If unsubscribe accidentally emitted
UnAdvise for a buffered correlation id, the engine would return
non-zero HRESULT (no matching plain advise to retract) — surfacing
as a panic.
Step 2 (F45 buffered recovery replay):
- crates/mxaccess-compat/tests/buffered_recovery_replay_live.rs.
- Subscribe buffered, drain >=1 NMX subscription message
(cmd=0x32 SubscriptionStatus + cmd=0x33 DataUpdate) to confirm the
wire path is hot pre-recovery, install a RebuildFactory that calls
NmxClient::create (the same auto-resolving COM-activation path
Session::connect_nmx_auto uses), invoke recover_connection, drain
>=1 NMX subscription message post-recovery.
- Verifies the replay branch in recover_connection_core re-issues
RegisterReference (NOT AdviseSupervisory) for the buffered entry,
mirroring MxNativeSession.ReAdviseSubscription (cs:538-569).
Structural property is unit-tested; this confirms the engine
actually picks back up after the rebuild + replay.
Both tests pass live on this Galaxy:
cargo test -p mxaccess-compat --features live-windows-com \
--test buffered_unsubscribe_skip_live -- --ignored --nocapture
cargo test -p mxaccess-compat --features live-windows-com \
--test buffered_recovery_replay_live -- --ignored --nocapture
Pulls mxaccess-nmx + mxaccess-codec into mxaccess-compat dev-deps so
the recovery test can build a RebuildFactory closure that returns
NmxClient and bind a typed broadcast Receiver.
design/followups.md F49 -> Resolved (all five steps pass live).
docs/M6-live-verification.md updated with per-step evidence + repro
commands.
F49 is fully closed out. F55 (DCOM-managed INmxSvcCallback, Path A)
and F56 (missing EnsurePublisherConnected + post-RegisterReference
AdviseSupervisory for buffered) were the two real Rust-port bugs
uncovered along the way; both resolved. Remaining post-V1 followups
(F50 Suspend/Activate Frida, F51 ASB type matrix, F52 perf, F53 doc
lint, etc.) are scoped independently and not part of F49.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
115 lines
4.3 KiB
Rust
115 lines
4.3 KiB
Rust
//! F49 step 3 — F47 buffered-unsubscribe skip live verification.
|
|
//!
|
|
//! `Session::unsubscribe` on a buffered subscription must NOT emit a
|
|
//! wire-side `UnAdvise` op (mirrors the .NET reference's
|
|
//! `if (!subscription.IsBuffered)` guard at `MxNativeSession.cs:361-381`).
|
|
//! Buffered subscriptions are unwound by the engine when the
|
|
//! `RegisterReference` handle goes away — there's no item-level advise
|
|
//! to retract.
|
|
//!
|
|
//! Structural verification is exhaustive at the unit level (see
|
|
//! `unsubscribe_skips_un_advise_for_buffered_subscription` in
|
|
//! `crates/mxaccess/src/session.rs`). This live test confirms the
|
|
//! behaviour against a real engine: subscribe buffered, immediately
|
|
//! unsubscribe, verify both calls succeed without surfacing transport
|
|
//! or HRESULT errors. If `unsubscribe` accidentally issued an
|
|
//! `UnAdvise` for a buffered correlation id, the engine would either
|
|
//! reject it (HRESULT != 0) or silently break the unrelated state —
|
|
//! both surface as a panic here.
|
|
|
|
#![allow(
|
|
clippy::unwrap_used,
|
|
clippy::expect_used,
|
|
clippy::indexing_slicing,
|
|
clippy::panic
|
|
)]
|
|
|
|
#[cfg(all(windows, feature = "live-windows-com"))]
|
|
mod live {
|
|
use std::sync::Arc;
|
|
|
|
use mxaccess::{BufferedOptions, RecoveryPolicy, Session, SessionOptions};
|
|
use mxaccess_galaxy::SqlTagResolver;
|
|
use mxaccess_rpc::ntlm::NtlmClientContext;
|
|
|
|
fn ntlm_from_test_env() -> NtlmClientContext {
|
|
let user = std::env::var("MX_TEST_USER").expect("MX_TEST_USER");
|
|
let password = std::env::var("MX_TEST_PASSWORD").expect("MX_TEST_PASSWORD");
|
|
let domain = std::env::var("MX_TEST_DOMAIN").unwrap_or_default();
|
|
let hostname = std::env::var("COMPUTERNAME").unwrap_or_default();
|
|
NtlmClientContext::new(&user, &password, &domain, Some(&hostname))
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore]
|
|
async fn buffered_unsubscribe_skips_unadvise() {
|
|
if std::env::var_os("MX_LIVE").is_none() {
|
|
eprintln!("MX_LIVE not set — skipping live test");
|
|
return;
|
|
}
|
|
let tag = std::env::var("MX_TEST_TAG")
|
|
.unwrap_or_else(|_| "TestMachine_001.TestChangingInt".to_string());
|
|
|
|
let _ = tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
|
)
|
|
.with_test_writer()
|
|
.try_init();
|
|
|
|
let galaxy_db = std::env::var("MX_GALAXY_DB").expect("MX_GALAXY_DB");
|
|
let resolver = Arc::new(
|
|
SqlTagResolver::from_ado_string(&galaxy_db).expect("SqlTagResolver"),
|
|
);
|
|
|
|
let session = Session::connect_nmx_auto(
|
|
ntlm_from_test_env,
|
|
SessionOptions::default(),
|
|
resolver,
|
|
RecoveryPolicy::default(),
|
|
)
|
|
.await
|
|
.expect("connect_nmx_auto");
|
|
eprintln!("session connected");
|
|
|
|
let opts = BufferedOptions {
|
|
update_interval_ms: 1_000,
|
|
};
|
|
let sub = session
|
|
.subscribe_buffered(&tag, opts)
|
|
.await
|
|
.expect("subscribe_buffered");
|
|
eprintln!(
|
|
"buffered subscribed, correlation_id = {:02x?}",
|
|
sub.correlation_id()
|
|
);
|
|
|
|
// Sub-second hold so the engine has at least one DataUpdate
|
|
// tick in flight when we unsubscribe.
|
|
tokio::time::sleep(std::time::Duration::from_millis(750)).await;
|
|
|
|
// The contract: unsubscribe on a buffered subscription
|
|
// returns Ok and does NOT issue UnAdvise on the wire.
|
|
// If it incorrectly emitted UnAdvise for a buffered
|
|
// correlation id, the engine would return non-zero HRESULT
|
|
// (no matching plain advise to retract) and surface here.
|
|
session
|
|
.unsubscribe(sub)
|
|
.await
|
|
.expect("unsubscribe (buffered) must succeed without emitting UnAdvise");
|
|
eprintln!("buffered unsubscribe returned Ok — F47 skip path verified live");
|
|
|
|
session.shutdown_nmx().await.expect("shutdown");
|
|
}
|
|
}
|
|
|
|
#[cfg(not(all(windows, feature = "live-windows-com")))]
|
|
mod live {
|
|
#[test]
|
|
#[ignore]
|
|
fn buffered_unsubscribe_skips_unadvise() {
|
|
eprintln!("test skipped: requires Windows + live-windows-com feature");
|
|
}
|
|
}
|