Skip to content

Commit aea52c0

Browse files
authored
Merge pull request alibaba#313 from liuxiaopai-ai/codex/csharp-ready-timeout-diagnostics
feat(csharp-sdk): improve ready-timeout diagnostics
2 parents e32e5c0 + 9be3c02 commit aea52c0

File tree

2 files changed

+157
-1
lines changed

2 files changed

+157
-1
lines changed

sdks/sandbox/csharp/src/OpenSandbox/Sandbox.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,16 +521,25 @@ public async Task WaitUntilReadyAsync(
521521
{
522522
_logger.LogDebug("Start readiness check for sandbox {SandboxId} (timeoutSeconds={TimeoutSeconds})", Id, options.ReadyTimeoutSeconds);
523523
var deadline = DateTime.UtcNow.AddSeconds(options.ReadyTimeoutSeconds);
524+
var attempt = 0;
525+
var errorDetail = "Health check returned false continuously.";
524526

525527
while (true)
526528
{
527529
cancellationToken.ThrowIfCancellationRequested();
528530

529531
if (DateTime.UtcNow > deadline)
530532
{
533+
var context = $"domain={ConnectionConfig.Domain}, useServerProxy={ConnectionConfig.UseServerProxy}";
534+
var suggestion = "If this sandbox runs in Docker bridge or remote-network mode, consider enabling useServerProxy=true.";
535+
if (!ConnectionConfig.UseServerProxy)
536+
{
537+
suggestion += " You can also configure server-side [docker].host_ip for direct endpoint access.";
538+
}
531539
throw new SandboxReadyTimeoutException(
532-
$"Sandbox not ready: timed out waiting for health check (timeoutSeconds={options.ReadyTimeoutSeconds})");
540+
$"Sandbox health check timed out after {options.ReadyTimeoutSeconds}s ({attempt} attempts). {errorDetail} Connection context: {context}. {suggestion}");
533541
}
542+
attempt++;
534543

535544
try
536545
{
@@ -549,10 +558,13 @@ public async Task WaitUntilReadyAsync(
549558
_logger.LogInformation("Sandbox is ready: {SandboxId}", Id);
550559
return;
551560
}
561+
562+
errorDetail = "Health check returned false continuously.";
552563
}
553564
catch (Exception ex)
554565
{
555566
_logger.LogDebug(ex, "Readiness probe failed for sandbox {SandboxId}", Id);
567+
errorDetail = $"Last health check error: {ex.Message}";
556568
}
557569

558570
await Task.Delay(options.PollingIntervalMillis, cancellationToken).ConfigureAwait(false);
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright 2026 Alibaba Group Holding Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using FluentAssertions;
16+
using Moq;
17+
using OpenSandbox.Config;
18+
using OpenSandbox.Core;
19+
using OpenSandbox.Factory;
20+
using OpenSandbox.Models;
21+
using OpenSandbox.Services;
22+
using Xunit;
23+
24+
namespace OpenSandbox.Tests;
25+
26+
public class SandboxReadinessDiagnosticsTests
27+
{
28+
[Fact]
29+
public async Task WaitUntilReadyAsync_WhenHealthCheckThrows_IncludesLastErrorAndConnectionContext()
30+
{
31+
// Arrange
32+
var healthMock = new Mock<IExecdHealth>();
33+
healthMock
34+
.Setup(x => x.PingAsync(It.IsAny<CancellationToken>()))
35+
.ThrowsAsync(new Exception("connect ECONNREFUSED 127.0.0.1:8080"));
36+
37+
var sandbox = await CreateSandboxForReadinessTestAsync(healthMock, useServerProxy: false);
38+
39+
// Act
40+
Func<Task> action = async () =>
41+
await sandbox.WaitUntilReadyAsync(new WaitUntilReadyOptions
42+
{
43+
ReadyTimeoutSeconds = 1,
44+
PollingIntervalMillis = 1
45+
});
46+
47+
// Assert
48+
try
49+
{
50+
var ex = await action.Should().ThrowAsync<SandboxReadyTimeoutException>();
51+
ex.Which.Message.Should().Contain("Sandbox health check timed out");
52+
ex.Which.Message.Should().Contain("Last health check error");
53+
ex.Which.Message.Should().Contain("domain=localhost:8080");
54+
ex.Which.Message.Should().Contain("useServerProxy=False");
55+
ex.Which.Message.Should().Contain("useServerProxy=true");
56+
ex.Which.Message.Should().Contain("[docker].host_ip");
57+
}
58+
finally
59+
{
60+
await sandbox.DisposeAsync();
61+
}
62+
}
63+
64+
[Fact]
65+
public async Task WaitUntilReadyAsync_WhenHealthCheckReturnsFalse_UsesFalseContinuouslyHint()
66+
{
67+
// Arrange
68+
var healthMock = new Mock<IExecdHealth>();
69+
healthMock
70+
.Setup(x => x.PingAsync(It.IsAny<CancellationToken>()))
71+
.ReturnsAsync(false);
72+
73+
var sandbox = await CreateSandboxForReadinessTestAsync(healthMock, useServerProxy: true);
74+
75+
// Act
76+
Func<Task> action = async () =>
77+
await sandbox.WaitUntilReadyAsync(new WaitUntilReadyOptions
78+
{
79+
ReadyTimeoutSeconds = 1,
80+
PollingIntervalMillis = 1
81+
});
82+
83+
// Assert
84+
try
85+
{
86+
var ex = await action.Should().ThrowAsync<SandboxReadyTimeoutException>();
87+
ex.Which.Message.Should().Contain("Health check returned false continuously.");
88+
ex.Which.Message.Should().Contain("useServerProxy=True");
89+
ex.Which.Message.Should().NotContain("[docker].host_ip");
90+
}
91+
finally
92+
{
93+
await sandbox.DisposeAsync();
94+
}
95+
}
96+
97+
private static async Task<Sandbox> CreateSandboxForReadinessTestAsync(
98+
Mock<IExecdHealth> healthMock,
99+
bool useServerProxy)
100+
{
101+
var sandboxesMock = new Mock<ISandboxes>();
102+
sandboxesMock
103+
.Setup(x => x.GetSandboxEndpointAsync(
104+
It.IsAny<string>(),
105+
It.IsAny<int>(),
106+
useServerProxy,
107+
It.IsAny<CancellationToken>()))
108+
.ReturnsAsync(new Endpoint
109+
{
110+
EndpointAddress = "127.0.0.1:44772",
111+
Headers = new Dictionary<string, string>()
112+
});
113+
114+
var adapterFactoryMock = new Mock<IAdapterFactory>();
115+
adapterFactoryMock
116+
.Setup(x => x.CreateLifecycleStack(It.IsAny<CreateLifecycleStackOptions>()))
117+
.Returns(new LifecycleStack
118+
{
119+
Sandboxes = sandboxesMock.Object
120+
});
121+
122+
adapterFactoryMock
123+
.Setup(x => x.CreateExecdStack(It.IsAny<CreateExecdStackOptions>()))
124+
.Returns(new ExecdStack
125+
{
126+
Commands = Mock.Of<IExecdCommands>(),
127+
Files = Mock.Of<ISandboxFiles>(),
128+
Health = healthMock.Object,
129+
Metrics = Mock.Of<IExecdMetrics>()
130+
});
131+
132+
return await Sandbox.ConnectAsync(new SandboxConnectOptions
133+
{
134+
SandboxId = "sbx-ready-diagnostics",
135+
ConnectionConfig = new ConnectionConfig(new ConnectionConfigOptions
136+
{
137+
Domain = "localhost:8080",
138+
UseServerProxy = useServerProxy
139+
}),
140+
AdapterFactory = adapterFactoryMock.Object,
141+
SkipHealthCheck = true
142+
});
143+
}
144+
}

0 commit comments

Comments
 (0)