fix(worker): classify ack interop failures at the COM call; ignore late completions
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m24s
ci / java (push) Successful in 2m32s
ci / nightly-windev (pull_request) Has been skipped
ci / java (pull_request) Successful in 2m38s
ci / portable (push) Failing after 2m52s
ci / portable (pull_request) Failing after 2m53s
ci / windows-x86 (pull_request) Successful in 8m8s

Address review of the #164-#166 fixes:

- The HRESULT check could not tell interop from programming errors
  (a managed ArgumentException already defaults to 0x80070057). Wrap each
  MXAccess call (AuthenticateUser, AddItem, Advise, Write) in a narrow
  filter that converts an interop failure to a private marker exception,
  and catch only that marker, so exceptions from the writer's own code
  (argument checks, caching, the pump) propagate.
- A late OnWriteComplete for a dropped handle recreated the cache entry
  that DropItem removed. The writer now records completions only for item
  handles it holds on its live registration.
- The item-handle-reuse probe now asserts that handles within one
  registration strictly increase, so a rerun is a regression check.

Refs #164, #165, #166

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
This commit is contained in:
Joseph Doherty
2026-10-20 07:18:02 -04:00
parent bd82036d5f
commit 463748e395
6 changed files with 145 additions and 81 deletions
@@ -46,6 +46,9 @@ public sealed class ItemHandleReuseProbeTests
private readonly ITestOutputHelper output;
private readonly List<string> log = new List<string>();
// Item handles returned on the first registration, in the order they were handed out.
private readonly List<int> firstRegistrationHandles = new List<int>();
/// <summary>Initializes a new instance of the <see cref="ItemHandleReuseProbeTests"/> class.</summary>
/// <param name="output">Test output helper for logging.</param>
public ItemHandleReuseProbeTests(ITestOutputHelper output)
@@ -76,6 +79,16 @@ public sealed class ItemHandleReuseProbeTests
{
throw new InvalidOperationException("Probe failed: " + threadException.Message, threadException);
}
// The ack writer's drop-and-re-add mitigation depends on this: within one
// registration a handle number is never handed out twice.
Assert.Equal((2 * Rounds) + 5, firstRegistrationHandles.Count);
for (int i = 1; i < firstRegistrationHandles.Count; i++)
{
Assert.True(
firstRegistrationHandles[i] > firstRegistrationHandles[i - 1],
$"Item handle {firstRegistrationHandles[i]} did not increase past {firstRegistrationHandles[i - 1]}; MXAccess reused or reordered item handles within one registration.");
}
}
private void RunProbe()
@@ -90,7 +103,7 @@ public sealed class ItemHandleReuseProbeTests
// A: the AlarmAckWriter drop path (Advise, then UnAdvise + RemoveItem).
for (int i = 0; i < Rounds; i++)
{
int h = mx.AddItem(server, AckItem);
int h = Held(mx.AddItem(server, AckItem));
mx.Advise(server, h);
Log($"A{i}: AddItem+Advise {AckItem} -> {h}");
mx.UnAdvise(server, h);
@@ -101,28 +114,28 @@ public sealed class ItemHandleReuseProbeTests
// B: RemoveItem without Advise.
for (int i = 0; i < Rounds; i++)
{
int h = mx.AddItem(server, AckItem);
int h = Held(mx.AddItem(server, AckItem));
Log($"B{i}: AddItem {AckItem} -> {h}");
mx.RemoveItem(server, h);
Log($"B{i}: RemoveItem {h}");
}
// C: interleave another item to see whether a freed number is handed to any item.
int c1 = mx.AddItem(server, AckItem);
int c1 = Held(mx.AddItem(server, AckItem));
Log($"C: AddItem {AckItem} -> {c1}");
mx.RemoveItem(server, c1);
Log($"C: RemoveItem {c1}");
int c2 = mx.AddItem(server, OtherItem);
int c2 = Held(mx.AddItem(server, OtherItem));
Log($"C: AddItem {OtherItem} -> {c2}");
int c3 = mx.AddItem(server, AckItem);
int c3 = Held(mx.AddItem(server, AckItem));
Log($"C: AddItem {AckItem} -> {c3}");
mx.RemoveItem(server, c2);
mx.RemoveItem(server, c3);
Log($"C: RemoveItem {c2}, {c3}");
// D: the same item added twice while the first is still held.
int d1 = mx.AddItem(server, AckItem);
int d2 = mx.AddItem(server, AckItem);
int d1 = Held(mx.AddItem(server, AckItem));
int d2 = Held(mx.AddItem(server, AckItem));
Log($"D: AddItem {AckItem} twice while held -> {d1}, {d2}");
mx.RemoveItem(server, d1);
mx.RemoveItem(server, d2);
@@ -159,6 +172,12 @@ public sealed class ItemHandleReuseProbeTests
}
}
private int Held(int itemHandle)
{
firstRegistrationHandles.Add(itemHandle);
return itemHandle;
}
private void Log(string line)
{
log.Add(DateTime.Now.ToString("HH:mm:ss.fff") + " " + line);