[F56] subscribe / subscribe_buffered: split-form wire body + diagnose Galaxy fixture gap
Three real fixes + one architectural diagnosis:
1. Session::subscribe_buffered_nmx now sends the .NET-reference split
form on the wire:
item_definition = "<attr>.property(buffer)" (was: full reference)
item_context = "<object_tag_name>" (was: empty)
item_handle = SessionInner::next_item_handle.fetch_add(1)
(was: hardcoded 0)
Verified byte-identical against captures/082 + 094 by the existing
buffered_register_reference_parity unit tests. The
item_handle counter mirrors MxNativeCompatibilityServer's
_nextItemHandle++ at MxNativeSession.cs:613.
2. New live tests:
- tests/buffered_subscribe_live.rs (F49 step 1) — uses real Galaxy
metadata via SqlTagResolver + connect_nmx_auto, drives a
background writer at 500ms cadence to force value-changes,
drains DataChange events from Subscription.
- tests/plain_subscribe_live.rs — same harness over plain
Session::subscribe (NOT buffered), used to isolate whether
"no DataUpdate" is buffered-specific (it's not — both fail).
Both pull tracing-subscriber as a dev-dep so `RUST_LOG=trace`
surfaces dcom_sink + router activity.
3. mxaccess-galaxy/sql_resolver.rs: drop the inner-attribute
`#![cfg(feature = "galaxy-resolver")]` — the module-level cfg on
`pub mod sql_resolver` in lib.rs already handles this and Rust
1.85's clippy::duplicated_attributes lint flagged the duplicate
once mxaccess-compat dev-deps activated the feature.
4. F56 finding (diagnosis, NOT a bug fix): the engine on this Galaxy
install does not have an active value for TestChildObject.TestInt.
Confirmed by running the .NET reference's own probe:
dotnet run --project src/MxNativeClient.Probe -c Release \
-- --probe-session-subscribe --tag=TestChildObject.TestInt \
--subscribe-hold-seconds=10
...returns ONE 0x32 SubscriptionStatus (status=3 detail=3
quality=0x00C0 Uncertain value=null) and zero 0x33 DataUpdates —
matching the Rust port's symptom exactly. Not a Rust port bug,
not a wire-byte gap. F49 steps 1-3 need either an actively-
scanned tag or local Galaxy reconfiguration to scan
TestChildObject.TestInt.
Workspace tests + clippy clean under both feature configurations.
F56 entry in design/followups.md updated with the full diagnostic
chain so future-me / future-collaborators can pick it up without
re-tracing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
//! Plain (non-buffered) subscribe live diagnostic for F49 / F56.
|
||||
//!
|
||||
//! Mirror of `buffered_subscribe_live.rs` but invokes
|
||||
//! `Session::subscribe` instead of `subscribe_buffered`. Used to
|
||||
//! isolate whether F56's "no DataUpdate" symptom is buffered-specific
|
||||
//! (only `subscribe_buffered` broken) or affects all subscribe paths.
|
||||
|
||||
#![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 std::time::{Duration, Instant};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use mxaccess::{MxValue, 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 plain_subscribe_yields_updates() {
|
||||
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(|_| "TestChildObject.TestInt".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 mut sub = session.subscribe(&tag).await.expect("subscribe");
|
||||
eprintln!("plain subscribe correlation_id = {:02x?}", sub.correlation_id());
|
||||
|
||||
// Background writer to force value changes.
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
let writer_session = session.clone();
|
||||
let writer_tag = tag.clone();
|
||||
let writer_stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let writer_stop_clone = writer_stop.clone();
|
||||
let writer = tokio::spawn(async move {
|
||||
let mut value: i32 = 2_000;
|
||||
while !writer_stop_clone.load(std::sync::atomic::Ordering::Acquire) {
|
||||
if writer_session
|
||||
.write(&writer_tag, MxValue::Int32(value))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
value = value.wrapping_add(1);
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
value
|
||||
});
|
||||
|
||||
let mut received = 0;
|
||||
while received < 2 && Instant::now() < deadline {
|
||||
match tokio::time::timeout(Duration::from_secs(5), sub.next()).await {
|
||||
Ok(Some(Ok(dc))) => {
|
||||
eprintln!("[{received}] {} = {:?} ts={:?}", dc.reference, dc.value, dc.timestamp);
|
||||
received += 1;
|
||||
}
|
||||
Ok(Some(Err(e))) => {
|
||||
writer_stop.store(true, std::sync::atomic::Ordering::Release);
|
||||
let _ = writer.await;
|
||||
panic!("subscription error: {e}");
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(_) => eprintln!("5s gap waiting for next update"),
|
||||
}
|
||||
}
|
||||
writer_stop.store(true, std::sync::atomic::Ordering::Release);
|
||||
let _ = writer.await;
|
||||
|
||||
assert!(received >= 1, "no DataChange arrived for plain subscribe");
|
||||
eprintln!("received {received} updates via plain subscribe");
|
||||
|
||||
session.unsubscribe(sub).await.expect("unsubscribe");
|
||||
session.shutdown_nmx().await.expect("shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(windows, feature = "live-windows-com")))]
|
||||
mod live {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn plain_subscribe_yields_updates() {
|
||||
eprintln!("test skipped: requires Windows + live-windows-com feature");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user