feat(worker): correlate OnWriteComplete onto plain Write/Write2 replies (06/S-1 follow-up)

OtOpcUa's dominant FreeAccess write path goes out as MX_COMMAND_KIND_WRITE,
not WriteSecured — the original 06/S-1 brief mis-scoped the correlation, so
a refused plain write was invisible on the unary reply (verified live on
windev 2026-08-09). ExecuteWrite/ExecuteWrite2 now use the same pre-call
version baseline + bounded pump-wait as the secured kinds. Bulk writes stay
fire-and-forget.
This commit is contained in:
Joseph Doherty
2026-08-09 19:47:00 -04:00
parent b948e6975e
commit b0e65d4f31
4 changed files with 170 additions and 35 deletions
@@ -8012,6 +8012,11 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
}
/// <summary>
/// The unary reply's statuses field carries the correlated OnWriteComplete
/// outcome when it arrives within the worker's bounded wait — see
/// MxCommandReply.statuses.
/// </summary>
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
public sealed partial class WriteCommand : pb::IMessage<WriteCommand>
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
@@ -8330,6 +8335,9 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
}
/// <summary>
/// Same statuses correlation as WriteCommand.
/// </summary>
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
public sealed partial class Write2Command : pb::IMessage<Write2Command>
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
@@ -17215,18 +17223,19 @@ namespace ZB.MOM.WW.MxGateway.Contracts.Proto {
= pb::FieldCodec.ForMessage(58, global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusProxy.Parser);
private readonly pbc::RepeatedField<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusProxy> statuses_ = new pbc::RepeatedField<global::ZB.MOM.WW.MxGateway.Contracts.Proto.MxStatusProxy>();
/// <summary>
/// Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
/// replies the worker holds the reply for a bounded window (default 1.5 s,
/// MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
/// MXAccess OnWriteComplete callback and copies its status rows here, so
/// statuses[0] carries the real MXAccess commit outcome (success OR failure)
/// while protocol_status/hresult still describe command acceptance only.
/// Empty statuses on a write reply means the completion did not arrive
/// Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
/// WRITE_SECURED2 replies the worker holds the reply for a bounded window
/// (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
/// the matching MXAccess OnWriteComplete callback and copies its status rows
/// here, so statuses[0] carries the real MXAccess commit outcome (success OR
/// failure) while protocol_status/hresult still describe command acceptance
/// only. Empty statuses on a write reply means the completion did not arrive
/// within the window — the write is unconfirmed, not failed. Correlation is
/// best-effort per (server_handle, item_handle): MXAccess's callback carries
/// no transaction id, so concurrent writes to the same item within the
/// window can swap rows. The OnWriteComplete event still flows on the event
/// stream unchanged. Other command kinds leave this field as before.
/// stream unchanged. Bulk write kinds and all non-write kinds leave this
/// field as before.
/// </summary>
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
@@ -241,6 +241,9 @@ message ActivateCommand {
int32 item_handle = 2;
}
// The unary reply's statuses field carries the correlated OnWriteComplete
// outcome when it arrives within the worker's bounded wait — see
// MxCommandReply.statuses.
message WriteCommand {
int32 server_handle = 1;
int32 item_handle = 2;
@@ -248,6 +251,7 @@ message WriteCommand {
int32 user_id = 4;
}
// Same statuses correlation as WriteCommand.
message Write2Command {
int32 server_handle = 1;
int32 item_handle = 2;
@@ -531,18 +535,19 @@ message MxCommandReply {
// transport failures.
optional int32 hresult = 5;
MxValue return_value = 6;
// Correlated per-item outcome rows. For WRITE_SECURED / WRITE_SECURED2
// replies the worker holds the reply for a bounded window (default 1.5 s,
// MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for the matching
// MXAccess OnWriteComplete callback and copies its status rows here, so
// statuses[0] carries the real MXAccess commit outcome (success OR failure)
// while protocol_status/hresult still describe command acceptance only.
// Empty statuses on a write reply means the completion did not arrive
// Correlated per-item outcome rows. For WRITE / WRITE2 / WRITE_SECURED /
// WRITE_SECURED2 replies the worker holds the reply for a bounded window
// (default 1.5 s, MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS) waiting for
// the matching MXAccess OnWriteComplete callback and copies its status rows
// here, so statuses[0] carries the real MXAccess commit outcome (success OR
// failure) while protocol_status/hresult still describe command acceptance
// only. Empty statuses on a write reply means the completion did not arrive
// within the window — the write is unconfirmed, not failed. Correlation is
// best-effort per (server_handle, item_handle): MXAccess's callback carries
// no transaction id, so concurrent writes to the same item within the
// window can swap rows. The OnWriteComplete event still flows on the event
// stream unchanged. Other command kinds leave this field as before.
// stream unchanged. Bulk write kinds and all non-write kinds leave this
// field as before.
repeated MxStatusProxy statuses = 7;
string diagnostic_message = 8;