Skip to content
Closed
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
55 changes: 55 additions & 0 deletions src/Build.UnitTests/BackEnd/UnixNodeReuseFixes_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if NET

using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using Shouldly;
using Xunit;

namespace Microsoft.Build.UnitTests
{
/// <summary>
/// Tests for Unix node reuse bug fixes:
/// - SessionId = 0 on Unix (cross-terminal node reuse)
/// </summary>
public class UnixNodeReuseFixes_Tests
{
[Fact]
public void Handshake_OnUnix_SessionIdIsZero()
{
if (!NativeMethodsShared.IsUnixLike)
{
return;
}
Comment on lines +22 to +25

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests use if (!NativeMethodsShared.IsUnixLike) return;, which makes them silently pass on non-Unix platforms. Prefer [UnixOnlyFact] (or another conditional test attribute) so the test is reported as skipped instead of passing without assertions.

Copilot uses AI. Check for mistakes.

// Two handshakes created from different contexts should have the same
// session ID (0) on Unix, enabling cross-terminal node reuse.
var h1 = new Handshake(HandshakeOptions.NodeReuse);
var h2 = new Handshake(HandshakeOptions.NodeReuse);

// Same handshake key means same session ID was used
h1.GetKey().ShouldBe(h2.GetKey());
}
Comment on lines +19 to +34

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handshake_OnUnix_SessionIdIsZero doesn't actually validate that SessionId is 0: both Handshake instances are created in the same process, so their keys would match even if SessionId were non-zero (pre-fix). Consider asserting handshake.RetrieveHandshakeComponents().SessionId == 0 (and/or remove the redundant key-equality check).

Copilot uses AI. Check for mistakes.

[Fact]
public void Handshake_SessionIdComponent_IsZeroOnUnix()
{
if (!NativeMethodsShared.IsUnixLike)
{
return;
}
Comment on lines +39 to +42

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here: returning early when not on Unix causes a pass-without-testing on Windows. Use [UnixOnlyFact]/conditional attributes so the test runner correctly reports it as skipped.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with using OS-specific facts - we should have a full set available.


var handshake = new Handshake(HandshakeOptions.NodeReuse);

// Key format: "options salt major minor build private sessionId"
// Last component should be 0 on Unix
string key = handshake.GetKey();
string[] keyParts = key.Split(' ');
keyParts[keyParts.Length - 1].ShouldBe("0");
}
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ internal abstract partial class NodeProviderOutOfProcBase
/// </summary>
private const int TimeoutForNewNodeCreation = 30000;

/// <summary>
/// The amount of time to wait when attempting to reuse an existing idle node.
/// Must be long enough for a sleeping node to wake and respond to the handshake.
/// </summary>
private const int TimeoutForNodeReuse = 1000;

/// <summary>
/// The amount of time to wait for an out-of-proc node to exit.
/// </summary>
Expand Down Expand Up @@ -318,7 +324,7 @@ bool TryReuseAnyFromPossibleRunningNodes(int currentProcessId, int nodeId)
_processesToIgnore.TryAdd(nodeLookupKey, default);

// Attempt to connect to each process in turn.
Stream nodeStream = TryConnectToProcess(nodeToReuse.Id, 0 /* poll, don't wait for connections */, nodeLaunchData.Handshake, out HandshakeResult result);
Stream nodeStream = TryConnectToProcess(nodeToReuse.Id, TimeoutForNodeReuse, nodeLaunchData.Handshake, out HandshakeResult result);
if (nodeStream != null)
{
// Connection successful, use this node.
Expand Down
12 changes: 9 additions & 3 deletions src/Shared/CommunicationsUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,14 @@ protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string too
int sessionId = 0;
if (includeSessionId)
{
using var currentProcess = Process.GetCurrentProcess();
sessionId = currentProcess.SessionId;
if (NativeMethodsShared.IsWindows)
{
using var currentProcess = Process.GetCurrentProcess();
sessionId = currentProcess.SessionId;
}
// On Unix, getsid() returns the session leader PID which differs per terminal,
// preventing cross-terminal node reuse. Use 0 since Unix doesn't need
// RDP-style session isolation.
}
Comment on lines 289 to 300

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes Handshake SessionId behavior only in src/Shared/CommunicationsUtilities.cs. There is a separate handshake implementation in src/MSBuildTaskHost/CommunicationsUtilities.cs that still uses EnvironmentUtilities.ProcessSessionId (getsid on Unix), so cross-terminal TaskHost reuse may remain broken unless it's updated similarly (or the scope is clarified).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that these should be unified in the same way.


_handshakeComponents = IsNetTaskHost
Expand Down Expand Up @@ -409,7 +415,7 @@ internal static class CommunicationsUtilities
/// <summary>
/// The timeout to connect to a node.
/// </summary>
private const int DefaultNodeConnectionTimeout = 900 * 1000; // 15 minutes; enough time that a dev will typically do another build in this time
private const int DefaultNodeConnectionTimeout = 30 * 1000; // 30 seconds
Comment on lines 415 to +418

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DefaultNodeConnectionTimeout is reduced to 30s here, but src/MSBuildTaskHost/CommunicationsUtilities.cs still defaults to 15 minutes. If TaskHost processes also rely on this timeout for idle cleanup, they'll continue to linger much longer than worker nodes. Consider aligning the defaults (or documenting why TaskHost should remain different).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a controversial change - please split it out into a separate PR from the pure bugfixes for argument discussion.


/// <summary>
/// Whether to trace communications
Expand Down
4 changes: 3 additions & 1 deletion src/Shared/NodeEndpointOutOfProcBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ internal abstract class NodeEndpointOutOfProcBase : INodeEndpoint
#if NETCOREAPP2_1_OR_GREATER
/// <summary>
/// The amount of time to wait for the client to connect to the host.
/// Reduced from 60s to 5s so that failed reuse probes don't block idle nodes
/// from reaching their connection timeout check.
/// </summary>
private const int ClientConnectTimeout = 60000;
private const int ClientConnectTimeout = 5000;
Comment on lines 45 to +50

Copilot AI Mar 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reducing this to 5s, a couple of callsites still describe this as "wait a long time" (for handshake reads). Consider updating the wording to avoid confusion when diagnosing timeouts.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is reasonable to want to make more responsive - @rainersigwald this appears to lock out nodes because the message processing pump is single-threaded and serial execution, yeah?

#endif // NETCOREAPP2_1

/// <summary>
Expand Down