Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ jobs:
run: dotnet build src/NetMQ.sln /p:Configuration=Release /verbosity:minimal
- name: test net10.0
run: dotnet test -v n -p:ParallelizeTestCollections=false --configuration Release --no-build -f net10.0 src/NetMQ.Tests/NetMQ.Tests.csproj
macos:
runs-on: macos-latest
permissions:
contents: read
env:
DOTNET_NOLOGO: true
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- run: dotnet restore src/NetMQ.sln
- name: build
run: dotnet build src/NetMQ.sln /p:Configuration=Release /verbosity:minimal
- name: test net10.0
run: dotnet test -v n -p:ParallelizeTestCollections=false --configuration Release --no-build -f net10.0 src/NetMQ.Tests/NetMQ.Tests.csproj
windows:
runs-on: windows-latest
env:
Expand Down
20 changes: 20 additions & 0 deletions src/NetMQ.Tests/CleanupTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Threading;
using NetMQ.Sockets;
using Xunit;

Expand Down Expand Up @@ -62,5 +63,24 @@ public void NoBlockNoDispose()

NetMQConfig.Cleanup(block: false);
}

[Fact]
public void NoBlockCompletesInBoundedTime()
{
// Regression test for https://github.com/zeromq/netmq/issues/1040
// Cleanup(block: false) must return quickly even when a socket has not been
// disposed and the internal poller is actively calling Socket.Select.
// On macOS, Socket.Select can block indefinitely when passed both a read list
// and an error list, causing Cleanup to hang forever without this guard.
_ = new DealerSocket(">tcp://localhost:5557"); // intentionally not disposed

// Run cleanup on a background (daemon) thread so the process can still exit
// if the thread gets stuck. IsBackground = true prevents it from blocking
// process shutdown if a regression causes Cleanup to hang.
var thread = new Thread(() => NetMQConfig.Cleanup(block: false)) { IsBackground = true };
thread.Start();
Assert.True(thread.Join(TimeSpan.FromSeconds(10)),
"Cleanup(block: false) did not complete within 10 seconds");
}
}
}
73 changes: 18 additions & 55 deletions src/NetMQ/Core/Utils/Poller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ You should have received a copy of the GNU Lesser General Public License
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
#if !NETFRAMEWORK
using System.Runtime.InteropServices;
#endif
using System.Threading;

namespace NetMQ.Core.Utils
Expand Down Expand Up @@ -255,15 +252,22 @@ public void Stop()
m_stopping = true;
}

/// <summary>
/// Maximum timeout (in microseconds) passed to Socket.Select.
/// On macOS ARM64, <c>Socket.Select</c> can fail to wake up when a TCP
/// loopback socket becomes readable. Capping the timeout ensures the loop
/// re-evaluates <see cref="m_stopping"/> periodically so that
/// <see cref="Stop"/> requests are never missed.
/// </summary>
private const int MaxSelectTimeoutMicroseconds = 500_000; // 500,000 µs = 500 ms

/// <summary>
/// This method is the polling-loop that is invoked on a background thread when Start is called.
/// As long as Stop hasn't been called: execute the timers, and invoke the handler-methods on each of the saved PollSets.
/// </summary>
private void Loop()
{
var readList = new List<Socket>();
// var writeList = new List<Socket>();
var errorList = new List<Socket>();

while (!m_stopping)
{
Expand All @@ -275,28 +279,19 @@ private void Loop()
int timeout = ExecuteTimers();

readList.AddRange(m_checkRead.ToArray());
// writeList.AddRange(m_checkWrite.ToArray());
errorList.AddRange(m_checkError.ToArray());

try
{
timeout = timeout != 0 ? timeout * 1000 : -1;
#if NETFRAMEWORK
Socket.Select(readList, null, errorList, timeout);
#else
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// Socket.Select does not work properly on macOS .NET Core when readList and errorList are passed
// together. To avoid this problem, we call the Select function separately for errorList.
// Please refer to this issue: https://github.com/dotnet/corefx/issues/39617
SocketUtility.Select(readList, null, null, timeout);
SocketUtility.Select(null, null, errorList, timeout);
}
else
{
Socket.Select(readList, null, errorList, timeout);
}
#endif

// Cap the timeout so the loop wakes up periodically. This
// prevents an indefinite hang on platforms where Socket.Select
// does not reliably detect readability on TCP loopback pairs
// (the Signaler mechanism used by the Mailbox).
if (timeout < 0 || timeout > MaxSelectTimeoutMicroseconds)
timeout = MaxSelectTimeoutMicroseconds;

Socket.Select(readList, null, null, timeout);
}
catch (SocketException)
{
Expand All @@ -309,36 +304,6 @@ private void Loop()
if (pollSet.Cancelled)
continue;

// Invoke its handler's InEvent if it's in our error-list.
if (errorList.Contains(pollSet.Socket))
{
try
{
pollSet.Handler.InEvent();
}
catch (TerminatingException)
{
}
}

if (pollSet.Cancelled)
continue;

// // Invoke its handler's OutEvent if it's in our write-list.
// if (writeList.Contains(pollSet.Socket))
// {
// try
// {
// pollSet.Handler.OutEvent();
// }
// catch (TerminatingException)
// {
// }
// }
//
// if (pollSet.Cancelled)
// continue;

// Invoke its handler's InEvent if it's in our read-list.
if (readList.Contains(pollSet.Socket))
{
Expand All @@ -352,8 +317,6 @@ private void Loop()
}
}

errorList.Clear();
// writeList.Clear();
readList.Clear();

if (m_retired)
Expand Down
13 changes: 8 additions & 5 deletions src/NetMQ/NetMQConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,17 @@ internal static Ctx Context
/// <param name="block">Set to true when you want to make sure sockets send all pending messages</param>
public static void Cleanup(bool block = true)
{
Ctx? ctx;
lock (s_sync)
{
if (s_ctx != null)
{
s_ctx.Terminate(block);
s_ctx = null;
}
// Capture and clear the context reference while holding the lock, then
// call Terminate outside the lock so a long-running or stuck Terminate
// (e.g. on macOS with block:false) never prevents other threads from
// acquiring s_sync and observing that cleanup has already been initiated.
ctx = s_ctx;
s_ctx = null;
}
ctx?.Terminate(block);
}

/// <summary>
Expand Down
Loading