diff --git a/lmxproxy/docs/deviations.md b/lmxproxy/docs/deviations.md index c8e4321..e018a0b 100644 --- a/lmxproxy/docs/deviations.md +++ b/lmxproxy/docs/deviations.md @@ -44,12 +44,13 @@ Decisions made during implementation that differ from or extend the original pla **Why**: The `x-api-key` header always carries the caller's valid key (for interceptor auth). The `CheckApiKey` RPC is designed for clients to test whether a *different* key is valid, so it must check the body key independently. **How to apply**: `ScadaGrpcService` receives `ApiKeyService` as an optional constructor parameter. -## 7. Write completes synchronously (fire-and-forget) +## 7. Write uses fire-and-forget (OnWriteComplete callback not delivered) **Plan specified**: Wait for `OnWriteComplete` COM callback to confirm write success. -**Actual**: Write is confirmed synchronously — if `_lmxProxy.Write()` returns without throwing, the write succeeded. The `OnWriteComplete` callback is kept wired for diagnostic logging only. -**Why**: MxAccess completes supervisory writes synchronously. The `OnWriteComplete` COM callback never fires for simple supervisory writes, causing the original implementation to timeout waiting for a callback that would never arrive. This caused WriteAndReadBack and WriteBatchAndWait integration tests to fail. -**How to apply**: Do not await `OnWriteComplete` for write confirmation. The `Write()` COM call succeeding (not throwing a COM exception) is the confirmation. Clean up (UnAdvise + RemoveItem) happens immediately after the write in a finally block. +**Actual**: Write is confirmed by `_lmxProxy.Write()` returning without throwing. The `OnWriteComplete` callback is kept wired for diagnostic logging but never awaited. +**Why**: The MxAccess documentation (Write() Method, p.47) explicitly states: *"Upon completion of the write, your program receives notification of the success/failure status through the OnWriteComplete() event"* and *"that item should not be taken off advise or removed from the internal tables until the OnWriteComplete() event is received."* So `OnWriteComplete` **should** fire — the issue is COM event delivery, not MxAccess behavior. The MxAccess sample applications are all Windows Forms apps with a UI message loop (`Application.Run()`). COM event callbacks are delivered via the Windows message pump. Our v2 Host runs as a headless Topshelf Windows service with no message loop. `Write()` is called from a thread pool thread (`Task.Run`), and the `OnWriteComplete` callback needs to be marshaled back to the calling apartment — which can't happen without a message pump. `OnDataChange` works because MxAccess fires it proactively on its own internal thread whenever data changes. `OnWriteComplete` is a response to a specific `Write()` call and appears to require message-pump-based marshaling to deliver. +**Risk**: For simple supervisory writes, fire-and-forget is safe — if `Write()` returns without a COM exception, MxAccess accepted the write. However, for secured writes (error 1012) or verified writes (error 1013), `OnWriteComplete` is the only way to learn that the write was rejected and must be retried with `WriteSecured()`. If secured/verified writes are ever needed, this must be revisited — either by running a message pump on a dedicated thread or by using a polling-based confirmation. +**How to apply**: Do not await `OnWriteComplete` for write confirmation. The `Write()` COM call succeeding (not throwing a COM exception) is the confirmation. Clean up (UnAdvise + RemoveItem) happens immediately after the write in a finally block. Keep `OnWriteComplete` wired — if COM threading is ever fixed (e.g., dedicated STA thread with proper message pump), the callback could be re-enabled. ## 8. SubscriptionManager must create MxAccess COM subscriptions