5e11b30507
Root cause: `Session::subscribe` and `Session::subscribe_buffered_nmx`
were missing the `INmxService2::Connect` + `AddSubscriberEngine` RPC
pair that the .NET reference's `MxNativeSession.EnsurePublisherConnected`
(`cs:516-526`) issues before the first advise against a publishing
engine. Without those two RPCs, NmxSvc accepted the subscription
registration but the publishing engine never knew our engine was
subscribed — so it never dispatched DataUpdate frames back.
Diagnosis driven by wwtools/aalogcli reading
C:\ProgramData\ArchestrA\LogFiles. The user pointed at this tooling
which lit up the path.
Red herring: NmxSvc's `[Warning] NmxCallback->DataReceived ... failed
with error 0x{N}` log lines turned out to be normal log spam where N
is the bufferSize of the inbound call, not a real error code. The
.NET reference's own probe triggers identical entries while still
receiving DataUpdate frames successfully.
Fix:
- SessionInner::publisher_endpoints — per-session HashMap<(platform_id,
engine_id), ()> cache mirroring MxNativeSession._publisherEndpoints.
- Session::ensure_publisher_connected — issues Connect +
AddSubscriberEngine, once per publisher endpoint per session.
- Session::subscribe + subscribe_buffered_nmx — both call it before
the wire advise.
- subscribe_buffered_nmx — additionally issues AdviseSupervisory after
RegisterReference. The .NET reference's RegisterBufferedItemAsync
only calls RegisterReference, but on this AVEVA install
RegisterReference alone produces the registration result + heartbeat
callbacks without ever starting DataUpdate dispatch; AdviseSupervisory
unblocks the dispatch.
Live verification (`TestMachine_001.TestChangingInt`, a tag that
updates >1×/s):
cargo test -p mxaccess-compat --features live-windows-com \
--test plain_subscribe_live -- --ignored --nocapture
cargo test -p mxaccess-compat --features live-windows-com \
--test buffered_subscribe_live -- --ignored --nocapture
Both pass — `cmd=0x32` SubscriptionStatus + sequence of `cmd=0x33`
DataUpdate frames flow as expected. Tests assert on the raw
Session::callbacks() broadcast (not the typed Subscription::next
DataChange path) because the engine reports quality=Uncertain
value=null for this attribute on this Galaxy — the wire-level
subscription is what F56 was about, not the value content.
DcomCallbackSink reverted to S_OK return for both DataReceivedRaw
and StatusReceivedRaw (the bytes-processed / sentinel HRESULT
experiments during diagnosis turned out to be irrelevant — the
"failed with error 0xN" logs come from NmxSvc regardless of the
return value).
design/followups.md F49 + F56 + docs/M6-live-verification.md updated:
F56 resolved, F49 steps 1 + 4 + 5 pass live, steps 2 + 3 pending
(now executable on this fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
4.1 KiB
Rust
110 lines
4.1 KiB
Rust
//! 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 mxaccess::{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");
|
|
|
|
// F56 — check raw NMX subscription messages on the broadcast,
|
|
// not the value-filtered Subscription stream. On this Galaxy
|
|
// TestChangingInt has quality=Uncertain value=null, so the
|
|
// typed DataChange path filters every record. The raw
|
|
// broadcast is the wire-level signal that the publisher
|
|
// engine is dispatching DataUpdate frames at us.
|
|
let mut callbacks_rx = session.callbacks();
|
|
let sub = session.subscribe(&tag).await.expect("subscribe");
|
|
eprintln!("plain subscribe correlation_id = {:02x?}", sub.correlation_id());
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(20);
|
|
let mut raw_received = 0;
|
|
while raw_received < 3 && Instant::now() < deadline {
|
|
match tokio::time::timeout(Duration::from_secs(5), callbacks_rx.recv()).await {
|
|
Ok(Ok(msg)) => {
|
|
eprintln!(
|
|
"[raw {raw_received}] cmd=0x{:02x} record_count={} records.len={}",
|
|
msg.command, msg.record_count, msg.records.len()
|
|
);
|
|
raw_received += 1;
|
|
}
|
|
Ok(Err(_)) => break,
|
|
Err(_) => eprintln!("5s gap waiting for next NMX message"),
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
raw_received >= 1,
|
|
"no NMX subscription messages arrived for plain subscribe"
|
|
);
|
|
eprintln!("received {raw_received} raw NMX subscription messages");
|
|
|
|
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");
|
|
}
|
|
}
|