fix(ab): orphan release clears the wrapper handle; rooting fallback documented and pinned (T21 review r1 M1/M2)
M2: after plc_tag_destroy, the release writes 0 back to the wrapper's nativeTagHandle, so a repeat release is a no-op instead of touching a destroyed id. The runtimes keep the destroyed handle for tests and diagnostics. M1: the rooting fallback is now described accurately, as an unbounded leak (one native tag per failed create), not a bounded one. A test seam (ForceRootingForTest) pins that the fallback roots the tag and destroys nothing. Both pins are red with their line removed. The orphan tests share a non-parallel collection, because the counters and the seam are process-wide. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -23,7 +23,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// <para>The wrapper exposes no handle, so it is read by reflection from the two private fields of the pinned
|
||||
/// 1.5.2 wrapper (<c>Tag._tag</c>, <c>NativeTagWrapper.nativeTagHandle</c>). If a future wrapper renames them the
|
||||
/// release cannot run; the tag is then rooted for the life of the process instead, which keeps its delegate
|
||||
/// alive (no crash, a bounded leak) — and <c>LibplctagOrphanReleaseTests</c> fails, so the bump is noticed.</para>
|
||||
/// alive. That is no crash but an UNBOUNDED leak — one native tag (holding its libplctag session open) per failed
|
||||
/// create, and the data path re-creates a NotFound tag on every read — so <c>LibplctagOrphanReleaseTests</c> fails, so the bump is noticed.</para>
|
||||
/// </remarks>
|
||||
internal static class LibplctagOrphanRelease
|
||||
{
|
||||
@@ -38,7 +39,10 @@ internal static class LibplctagOrphanRelease
|
||||
private static int _released;
|
||||
|
||||
/// <summary>True when the pinned wrapper's private fields were found, so a failed tag can be destroyed.</summary>
|
||||
internal static bool CanReadHandles => HandleField is not null;
|
||||
internal static bool CanReadHandles => HandleField is not null && !ForceRootingForTest;
|
||||
|
||||
/// <summary>Test seam (review r1 M1): take the rooting fallback as if the wrapper's fields had been renamed.</summary>
|
||||
internal static bool ForceRootingForTest { get; set; }
|
||||
|
||||
/// <summary>Orphaned native tags destroyed so far (process-wide; diagnostics and tests).</summary>
|
||||
internal static int Released => Volatile.Read(ref _released);
|
||||
@@ -50,6 +54,12 @@ internal static class LibplctagOrphanRelease
|
||||
internal static int HandleOf(Tag tag) =>
|
||||
HandleField is not null && WrapperField!.GetValue(tag) is { } wrapper ? (int)HandleField.GetValue(wrapper)! : 0;
|
||||
|
||||
/// <summary>Clears the wrapper's handle after a destroy, so a repeat release is a no-op (review r1 M2).</summary>
|
||||
private static void ClearHandle(Tag tag)
|
||||
{
|
||||
if (HandleField is not null && WrapperField!.GetValue(tag) is { } wrapper) HandleField.SetValue(wrapper, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a libplctag operation that may initialise <paramref name="tag"/> (InitializeAsync, or a first
|
||||
/// Read/Write, which initialise on demand). If it fails and leaves the tag uninitialised, the native tag it
|
||||
@@ -103,6 +113,7 @@ internal static class LibplctagOrphanRelease
|
||||
var status = (Status)plctag.plc_tag_status(handle);
|
||||
plctag.plc_tag_unregister_callback(handle);
|
||||
plctag.plc_tag_destroy(handle);
|
||||
ClearHandle(tag);
|
||||
Interlocked.Increment(ref _released);
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
||||
/// </summary>
|
||||
private Status? _releasedStatus;
|
||||
|
||||
/// <summary>The native handle a failed initialisation destroyed (0 if none); the wrapper's own field is cleared.</summary>
|
||||
private int _releasedHandle;
|
||||
|
||||
/// <summary>Initializes a new instance of the LibplctagTagRuntime class.</summary>
|
||||
/// <param name="p">The tag creation parameters.</param>
|
||||
public LibplctagTagRuntime(AbCipTagCreateParams p)
|
||||
@@ -70,16 +73,26 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
||||
{
|
||||
await LibplctagStatusTranslation.Wrap(op).ConfigureAwait(false);
|
||||
_releasedStatus = null;
|
||||
_releasedHandle = 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (LibplctagOrphanRelease.ReleaseIfOrphaned(_tag) is { } status) _releasedStatus = status;
|
||||
var handle = LibplctagOrphanRelease.HandleOf(_tag);
|
||||
if (LibplctagOrphanRelease.ReleaseIfOrphaned(_tag) is { } status)
|
||||
{
|
||||
_releasedStatus = status;
|
||||
_releasedHandle = handle;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The native libplctag handle this runtime's tag holds or last held (0 if none was created). Tests only.</summary>
|
||||
internal int NativeHandle => LibplctagOrphanRelease.HandleOf(_tag);
|
||||
/// <summary>The native libplctag handle this runtime's tag holds, or the one a failed initialisation destroyed
|
||||
/// (0 if none was created). Tests only.</summary>
|
||||
internal int NativeHandle => _releasedHandle != 0 ? _releasedHandle : LibplctagOrphanRelease.HandleOf(_tag);
|
||||
|
||||
/// <summary>The handle the libplctag wrapper itself still holds (0 once a release cleared it). Tests only.</summary>
|
||||
internal int WrapperHandle => LibplctagOrphanRelease.HandleOf(_tag);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetStatus() =>
|
||||
|
||||
@@ -19,6 +19,9 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
/// </summary>
|
||||
private Status? _releasedStatus;
|
||||
|
||||
/// <summary>The native handle a failed initialisation destroyed (0 if none); the wrapper's own field is cleared.</summary>
|
||||
private int _releasedHandle;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="LibplctagLegacyTagRuntime"/> class.</summary>
|
||||
/// <param name="p">The parameters for tag creation.</param>
|
||||
public LibplctagLegacyTagRuntime(AbLegacyTagCreateParams p)
|
||||
@@ -66,16 +69,26 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
{
|
||||
await LibplctagLegacyStatusTranslation.Wrap(op).ConfigureAwait(false);
|
||||
_releasedStatus = null;
|
||||
_releasedHandle = 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (LibplctagOrphanRelease.ReleaseIfOrphaned(_tag) is { } status) _releasedStatus = status;
|
||||
var handle = LibplctagOrphanRelease.HandleOf(_tag);
|
||||
if (LibplctagOrphanRelease.ReleaseIfOrphaned(_tag) is { } status)
|
||||
{
|
||||
_releasedStatus = status;
|
||||
_releasedHandle = handle;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The native libplctag handle this runtime's tag holds or last held (0 if none was created). Tests only.</summary>
|
||||
internal int NativeHandle => LibplctagOrphanRelease.HandleOf(_tag);
|
||||
/// <summary>The native libplctag handle this runtime's tag holds, or the one a failed initialisation destroyed
|
||||
/// (0 if none was created). Tests only.</summary>
|
||||
internal int NativeHandle => _releasedHandle != 0 ? _releasedHandle : LibplctagOrphanRelease.HandleOf(_tag);
|
||||
|
||||
/// <summary>The handle the libplctag wrapper itself still holds (0 once a release cleared it). Tests only.</summary>
|
||||
internal int WrapperHandle => LibplctagOrphanRelease.HandleOf(_tag);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetStatus() =>
|
||||
|
||||
@@ -23,7 +23,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
/// <para>The wrapper exposes no handle, so it is read by reflection from the two private fields of the pinned
|
||||
/// 1.5.2 wrapper (<c>Tag._tag</c>, <c>NativeTagWrapper.nativeTagHandle</c>). If a future wrapper renames them the
|
||||
/// release cannot run; the tag is then rooted for the life of the process instead, which keeps its delegate
|
||||
/// alive (no crash, a bounded leak) — and <c>LibplctagLegacyOrphanReleaseTests</c> fails, so the bump is noticed.</para>
|
||||
/// alive. That is no crash but an UNBOUNDED leak — one native tag (holding its libplctag session open) per failed
|
||||
/// create, and the data path re-creates a NotFound tag on every read — so <c>LibplctagLegacyOrphanReleaseTests</c> fails, so the bump is noticed.</para>
|
||||
/// </remarks>
|
||||
internal static class LibplctagOrphanRelease
|
||||
{
|
||||
@@ -38,7 +39,10 @@ internal static class LibplctagOrphanRelease
|
||||
private static int _released;
|
||||
|
||||
/// <summary>True when the pinned wrapper's private fields were found, so a failed tag can be destroyed.</summary>
|
||||
internal static bool CanReadHandles => HandleField is not null;
|
||||
internal static bool CanReadHandles => HandleField is not null && !ForceRootingForTest;
|
||||
|
||||
/// <summary>Test seam (review r1 M1): take the rooting fallback as if the wrapper's fields had been renamed.</summary>
|
||||
internal static bool ForceRootingForTest { get; set; }
|
||||
|
||||
/// <summary>Orphaned native tags destroyed so far (process-wide; diagnostics and tests).</summary>
|
||||
internal static int Released => Volatile.Read(ref _released);
|
||||
@@ -50,6 +54,12 @@ internal static class LibplctagOrphanRelease
|
||||
internal static int HandleOf(Tag tag) =>
|
||||
HandleField is not null && WrapperField!.GetValue(tag) is { } wrapper ? (int)HandleField.GetValue(wrapper)! : 0;
|
||||
|
||||
/// <summary>Clears the wrapper's handle after a destroy, so a repeat release is a no-op (review r1 M2).</summary>
|
||||
private static void ClearHandle(Tag tag)
|
||||
{
|
||||
if (HandleField is not null && WrapperField!.GetValue(tag) is { } wrapper) HandleField.SetValue(wrapper, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a libplctag operation that may initialise <paramref name="tag"/> (InitializeAsync, or a first
|
||||
/// Read/Write, which initialise on demand). If it fails and leaves the tag uninitialised, the native tag it
|
||||
@@ -103,6 +113,7 @@ internal static class LibplctagOrphanRelease
|
||||
var status = (Status)plctag.plc_tag_status(handle);
|
||||
plctag.plc_tag_unregister_callback(handle);
|
||||
plctag.plc_tag_destroy(handle);
|
||||
ClearHandle(tag);
|
||||
Interlocked.Increment(ref _released);
|
||||
return status;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user