Skip to content

Commit b0f310d

Browse files
authored
feat: multi-url support (#59)
1 parent 1cae00d commit b0f310d

27 files changed

+2287
-173
lines changed

ci/azure-pipelines.yml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ steps:
6060
version: $(dotnetVersion)
6161
installationPath: $(Agent.ToolsDirectory)/dotnet
6262

63+
- task: UseDotNet@2
64+
displayName: 'Install .NET 10.0 SDK'
65+
inputs:
66+
packageType: 'sdk'
67+
version: '10.0.x'
68+
installationPath: $(Agent.ToolsDirectory)/dotnet
69+
6370
- task: DotNetCoreCLI@2
6471
displayName: 'Restore dependencies'
6572
inputs:
@@ -74,12 +81,22 @@ steps:
7481
arguments: '--configuration $(buildConfiguration) --no-restore'
7582

7683
- task: DotNetCoreCLI@2
77-
displayName: 'Run tests on $(osName)'
84+
displayName: 'Run all tests on $(osName)'
7885
inputs:
7986
command: 'test'
8087
projects: 'src/net-questdb-client-tests/net-questdb-client-tests.csproj'
8188
arguments: '--configuration $(buildConfiguration) --framework net9.0 --no-build --verbosity normal --logger trx --collect:"XPlat Code Coverage"'
8289
publishTestResults: true
90+
condition: eq(variables['osName'], 'Linux')
91+
92+
- task: DotNetCoreCLI@2
93+
displayName: 'Run tests on $(osName) (excluding integration tests)'
94+
inputs:
95+
command: 'test'
96+
projects: 'src/net-questdb-client-tests/net-questdb-client-tests.csproj'
97+
arguments: '--configuration $(buildConfiguration) --framework net9.0 --no-build --verbosity normal --logger trx --collect:"XPlat Code Coverage" --filter "FullyQualifiedName!~QuestDbIntegrationTests"'
98+
publishTestResults: true
99+
condition: ne(variables['osName'], 'Linux')
83100

84101
- task: PublishCodeCoverageResults@2
85102
displayName: 'Publish code coverage'

ci/azurre-binaries-pipeline.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pr: none
66

77
variables:
88
buildConfiguration: 'Release'
9-
dotnetVersion: '9.0.x'
9+
dotnetVersion: '10.0.x'
1010
nugetPackageDirectory: '$(Build.ArtifactStagingDirectory)'
1111

1212
pool:
@@ -46,6 +46,13 @@ stages:
4646

4747
- task: UseDotNet@2
4848
displayName: 'Install .NET 9.0 SDK'
49+
inputs:
50+
packageType: 'sdk'
51+
version: '9.0.x'
52+
installationPath: $(Agent.ToolsDirectory)/dotnet
53+
54+
- task: UseDotNet@2
55+
displayName: 'Install .NET 10.0 SDK'
4956
inputs:
5057
packageType: 'sdk'
5158
version: $(dotnetVersion)

global.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"sdk": {
3-
"version": "9.0.0",
3+
"version": "10.0.0",
44
"rollForward": "latestMajor",
55
"allowPrerelease": true
66
}
7-
}
7+
}

src/dummy-http-server/DummyHttpServer.cs

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ public class DummyHttpServer : IDisposable
4141
private readonly WebApplication _app;
4242
private int _port = 29743;
4343
private readonly TimeSpan? _withStartDelay;
44+
private readonly bool _withTokenAuth;
45+
private readonly bool _withBasicAuth;
46+
private readonly bool _withRetriableError;
47+
private readonly bool _withErrorMessage;
48+
private readonly bool _requireClientCert;
4449

4550
/// <summary>
4651
/// Initializes a configurable in-process dummy HTTP server used for testing endpoints.
@@ -63,11 +68,20 @@ public DummyHttpServer(bool withTokenAuth = false, bool withBasicAuth = false, b
6368
.AddConsole();
6469
});
6570

71+
// Store configuration in instance fields instead of static fields
72+
// to avoid interference between multiple concurrent servers
73+
_withTokenAuth = withTokenAuth;
74+
_withBasicAuth = withBasicAuth;
75+
_withRetriableError = withRetriableError;
76+
_withErrorMessage = withErrorMessage;
77+
_withStartDelay = withStartDelay;
78+
_requireClientCert = requireClientCert;
79+
80+
// Also set static flags for backwards compatibility
6681
IlpEndpoint.WithTokenAuth = withTokenAuth;
6782
IlpEndpoint.WithBasicAuth = withBasicAuth;
6883
IlpEndpoint.WithRetriableError = withRetriableError;
6984
IlpEndpoint.WithErrorMessage = withErrorMessage;
70-
_withStartDelay = withStartDelay;
7185

7286
if (withTokenAuth)
7387
{
@@ -91,16 +105,22 @@ public DummyHttpServer(bool withTokenAuth = false, bool withBasicAuth = false, b
91105
}
92106

93107
o.Limits.MaxRequestBodySize = 1073741824;
94-
o.ListenLocalhost(29474,
95-
options => { options.UseHttps(); });
96-
o.ListenLocalhost(29473);
108+
// Note: These internal ports will be set dynamically in StartAsync based on the main port
109+
// to avoid conflicts when multiple DummyHttpServer instances are created
97110
});
98111

99112
_app = bld.Build();
100113

101114
_app.MapHealthChecks("/ping");
102115
_app.UseDefaultExceptionHandler();
103116

117+
// Add middleware to set X-Server-Port header so endpoints know which port they're running on
118+
_app.Use(async (context, next) =>
119+
{
120+
context.Request.Headers["X-Server-Port"] = _port.ToString();
121+
await next();
122+
});
123+
104124
if (withTokenAuth)
105125
{
106126
_app
@@ -126,10 +146,7 @@ public void Dispose()
126146
/// </remarks>
127147
public void Clear()
128148
{
129-
IlpEndpoint.ReceiveBuffer.Clear();
130-
IlpEndpoint.ReceiveBytes.Clear();
131-
IlpEndpoint.LastError = null;
132-
IlpEndpoint.Counter = 0;
149+
IlpEndpoint.ClearPort(_port);
133150
}
134151

135152
/// <summary>
@@ -148,7 +165,18 @@ public async Task StartAsync(int port = 29743, int[]? versions = null)
148165
versions ??= new[] { 1, 2, 3, };
149166
SettingsEndpoint.Versions = versions;
150167
_port = port;
151-
_ = _app.RunAsync($"http://localhost:{port}");
168+
169+
// Store configuration flags keyed by port so multiple servers don't interfere
170+
IlpEndpoint.SetPortConfig(port,
171+
tokenAuth: _withTokenAuth,
172+
basicAuth: _withBasicAuth,
173+
retriableError: _withRetriableError,
174+
errorMessage: _withErrorMessage);
175+
176+
var url = _requireClientCert
177+
? $"https://localhost:{port}"
178+
: $"http://localhost:{port}";
179+
_ = _app.RunAsync(url);
152180
}
153181

154182
/// <summary>
@@ -170,7 +198,7 @@ public async Task StopAsync()
170198
/// <returns>The mutable <see cref="StringBuilder"/> containing the accumulated received text; modifying it updates the server's buffer.</returns>
171199
public StringBuilder GetReceiveBuffer()
172200
{
173-
return IlpEndpoint.ReceiveBuffer;
201+
return IlpEndpoint.GetReceiveBuffer(_port);
174202
}
175203

176204
/// <summary>
@@ -179,12 +207,12 @@ public StringBuilder GetReceiveBuffer()
179207
/// <returns>The mutable list of bytes received by the endpoint.</returns>
180208
public List<byte> GetReceivedBytes()
181209
{
182-
return IlpEndpoint.ReceiveBytes;
210+
return IlpEndpoint.GetReceiveBytes(_port);
183211
}
184212

185213
public Exception? GetLastError()
186214
{
187-
return IlpEndpoint.LastError;
215+
return IlpEndpoint.GetLastError(_port);
188216
}
189217

190218
public async Task<bool> Healthcheck()
@@ -215,7 +243,7 @@ public async Task<bool> Healthcheck()
215243

216244
public int GetCounter()
217245
{
218-
return IlpEndpoint.Counter;
246+
return IlpEndpoint.GetCounter(_port);
219247
}
220248

221249
/// <summary>

src/dummy-http-server/IlpEndpoint.cs

Lines changed: 132 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,117 @@ public class IlpEndpoint : Endpoint<Request, JsonErrorResponse?>
8383
{
8484
private const string Username = "admin";
8585
private const string Password = "quest";
86-
public static readonly StringBuilder ReceiveBuffer = new();
87-
public static readonly List<byte> ReceiveBytes = new();
88-
public static Exception? LastError = new();
86+
87+
// Port-keyed storage to support multiple concurrent DummyHttpServer instances
88+
private static readonly Dictionary<int, (StringBuilder Buffer, List<byte> Bytes, Exception? Error, int Counter)>
89+
PortData = new();
90+
91+
// Port-keyed configuration to support multiple concurrent DummyHttpServer instances
92+
private static readonly Dictionary<int, (bool TokenAuth, bool BasicAuth, bool RetriableError, bool ErrorMessage)>
93+
PortConfig = new();
94+
95+
// Configuration flags (global, apply to all servers) - kept for backwards compatibility
8996
public static bool WithTokenAuth = false;
9097
public static bool WithBasicAuth = false;
9198
public static bool WithRetriableError = false;
9299
public static bool WithErrorMessage = false;
93-
public static int Counter;
100+
101+
// Get the port from request headers (set by DummyHttpServer)
102+
private static int GetPortKey(HttpContext context)
103+
{
104+
if (context?.Request.Headers.TryGetValue("X-Server-Port", out var portHeader) == true
105+
&& int.TryParse(portHeader.ToString(), out var port))
106+
{
107+
return port;
108+
}
109+
return context?.Connection?.LocalPort ?? 0;
110+
}
111+
112+
private static (StringBuilder Buffer, List<byte> Bytes, Exception? Error, int Counter) GetOrCreatePortData(int port)
113+
{
114+
lock (PortData)
115+
{
116+
if (!PortData.TryGetValue(port, out var data))
117+
{
118+
data = (new StringBuilder(), new List<byte>(), null, 0);
119+
PortData[port] = data;
120+
}
121+
return data;
122+
}
123+
}
124+
125+
126+
// Public methods for accessing port-specific data (used by DummyHttpServer)
127+
public static StringBuilder GetReceiveBuffer(int port) => GetOrCreatePortData(port).Buffer;
128+
public static List<byte> GetReceiveBytes(int port) => GetOrCreatePortData(port).Bytes;
129+
130+
public static Exception? GetLastError(int port)
131+
{
132+
lock (PortData)
133+
{
134+
return GetOrCreatePortData(port).Error;
135+
}
136+
}
137+
138+
public static void SetLastError(int port, Exception? error)
139+
{
140+
lock (PortData)
141+
{
142+
var data = GetOrCreatePortData(port);
143+
PortData[port] = (data.Buffer, data.Bytes, error, data.Counter);
144+
}
145+
}
146+
147+
public static int GetCounter(int port)
148+
{
149+
lock (PortData)
150+
{
151+
return GetOrCreatePortData(port).Counter;
152+
}
153+
}
154+
155+
public static void SetCounter(int port, int value)
156+
{
157+
lock (PortData)
158+
{
159+
var data = GetOrCreatePortData(port);
160+
PortData[port] = (data.Buffer, data.Bytes, data.Error, value);
161+
}
162+
}
163+
164+
public static void ClearPort(int port)
165+
{
166+
lock (PortData)
167+
{
168+
if (PortData.TryGetValue(port, out var data))
169+
{
170+
data.Buffer.Clear();
171+
data.Bytes.Clear();
172+
PortData[port] = (data.Buffer, data.Bytes, null, 0);
173+
}
174+
}
175+
}
176+
177+
public static void SetPortConfig(int port, bool tokenAuth, bool basicAuth, bool retriableError, bool errorMessage)
178+
{
179+
lock (PortConfig)
180+
{
181+
PortConfig[port] = (tokenAuth, basicAuth, retriableError, errorMessage);
182+
}
183+
}
184+
185+
private static (bool TokenAuth, bool BasicAuth, bool RetriableError, bool ErrorMessage) GetPortConfig(int port)
186+
{
187+
lock (PortConfig)
188+
{
189+
if (PortConfig.TryGetValue(port, out var config))
190+
{
191+
return config;
192+
}
193+
// Return static flags as defaults for backwards compatibility
194+
return (WithTokenAuth, WithBasicAuth, WithRetriableError, WithErrorMessage);
195+
}
196+
}
94197

95198
public override void Configure()
96199
{
@@ -111,14 +214,24 @@ public override void Configure()
111214

112215
public override async Task HandleAsync(Request req, CancellationToken ct)
113216
{
114-
Counter++;
115-
if (WithRetriableError)
217+
int port = GetPortKey(HttpContext);
218+
var data = GetOrCreatePortData(port);
219+
var config = GetPortConfig(port);
220+
221+
lock (PortData)
222+
{
223+
// Increment counter for this port
224+
data = GetOrCreatePortData(port);
225+
PortData[port] = (data.Buffer, data.Bytes, data.Error, data.Counter + 1);
226+
}
227+
228+
if (config.RetriableError)
116229
{
117230
await SendAsync(null, 500, ct);
118231
return;
119232
}
120233

121-
if (WithErrorMessage)
234+
if (config.ErrorMessage)
122235
{
123236
await SendAsync(new JsonErrorResponse
124237
{ code = "code", errorId = "errorid", line = 1, message = "message", }, 400, ct);
@@ -127,13 +240,22 @@ await SendAsync(new JsonErrorResponse
127240

128241
try
129242
{
130-
ReceiveBuffer.Append(req.StringContent);
131-
ReceiveBytes.AddRange(req.ByteContent);
243+
lock (PortData)
244+
{
245+
data = GetOrCreatePortData(port);
246+
data.Buffer.Append(req.StringContent);
247+
data.Bytes.AddRange(req.ByteContent);
248+
PortData[port] = data;
249+
}
132250
await SendNoContentAsync(ct);
133251
}
134252
catch (Exception ex)
135253
{
136-
LastError = ex;
254+
lock (PortData)
255+
{
256+
data = GetOrCreatePortData(port);
257+
PortData[port] = (data.Buffer, data.Bytes, ex, data.Counter);
258+
}
137259
throw;
138260
}
139261
}

src/dummy-http-server/dummy-http-server.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
4-
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
4+
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0;net10.0</TargetFrameworks>
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<RootNamespace>dummy_http_server</RootNamespace>

src/example-aot/example-aot.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net9.0</TargetFramework>
5+
<TargetFramework>net10.0</TargetFramework>
66
<RootNamespace>example_aot</RootNamespace>
77
<ImplicitUsings>enable</ImplicitUsings>
88
<Nullable>enable</Nullable>

src/example-auth-http-tls/example-auth-http-tls.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
5+
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0;net10.0</TargetFrameworks>
66
<RootNamespace>example_auth_http_tls</RootNamespace>
77
<ImplicitUsings>enable</ImplicitUsings>
88
<Nullable>enable</Nullable>

0 commit comments

Comments
 (0)