Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
58c27c7
safety
nwoolmer Nov 14, 2025
9789045
safety
nwoolmer Nov 14, 2025
baa3beb
iterate
nwoolmer Dec 1, 2025
3e9a8fe
iterate
nwoolmer Dec 1, 2025
86ff775
iterate
nwoolmer Dec 1, 2025
f52e685
test fixes
nwoolmer Dec 1, 2025
6a8461b
iterate
nwoolmer Dec 1, 2025
9524a5e
fix tests
nwoolmer Dec 2, 2025
fa23f75
tests
nwoolmer Dec 2, 2025
c22783e
tests
nwoolmer Dec 2, 2025
bd5fcea
fixes
nwoolmer Dec 2, 2025
4bfc0ea
Fix authentication tests by simplifying to HTTP protocol
nwoolmer Dec 2, 2025
fdcdcbb
iterate
nwoolmer Dec 3, 2025
298c172
fix test
nwoolmer Dec 3, 2025
0c60393
merge
nwoolmer Dec 3, 2025
0ccafb9
cleanup api
nwoolmer Dec 3, 2025
e2c6ade
guid, char
nwoolmer Dec 3, 2025
758136c
cleanup
nwoolmer Dec 3, 2025
b7c3778
wait all
nwoolmer Dec 4, 2025
c0c81b0
address comment
nwoolmer Dec 4, 2025
787fcbf
Merge remote-tracking branch 'origin/feat-multi-url' into feat-multi-url
nwoolmer Dec 4, 2025
97a054b
address comment
nwoolmer Dec 4, 2025
5798bf0
address comments
nwoolmer Dec 4, 2025
20a85e3
comments
nwoolmer Dec 4, 2025
413632a
comments
nwoolmer Dec 4, 2025
818f09f
iterate on ci
nwoolmer Dec 4, 2025
e0359bb
ci iterate
nwoolmer Dec 4, 2025
608688b
ci
nwoolmer Dec 4, 2025
20c7734
ci
nwoolmer Dec 4, 2025
d7b9bf4
ci
nwoolmer Dec 4, 2025
2788ddd
ci
nwoolmer Dec 4, 2025
4f291ba
ci
nwoolmer Dec 4, 2025
bcd6420
ci
nwoolmer Dec 4, 2025
c6182a0
ci
nwoolmer Dec 4, 2025
4847828
ci
nwoolmer Dec 4, 2025
e49f7d3
ci
nwoolmer Dec 4, 2025
0b711e5
ci
nwoolmer Dec 4, 2025
bec69ed
Update ci/azure-pipelines.yml
nwoolmer Dec 5, 2025
9770424
Apply suggestions from code review
nwoolmer Dec 5, 2025
742b07c
address https://github.com/questdb/net-questdb-client/issues/50
nwoolmer Dec 5, 2025
7c711de
Merge remote-tracking branch 'origin/feat-multi-url' into feat-multi-url
nwoolmer Dec 5, 2025
9141a80
iterate
nwoolmer Dec 5, 2025
b87d24f
ci
nwoolmer Dec 5, 2025
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
74 changes: 74 additions & 0 deletions ci/azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,80 @@ steps:
projects: 'net-questdb-client.sln'
arguments: '--configuration $(buildConfiguration) --no-restore'

- script: |
sudo apt-get update
sudo apt-get install -y questdb
sudo systemctl start questdb
sudo systemctl enable questdb
# Wait for QuestDB to be ready
for i in {1..60}; do
if curl -s http://localhost:9000/settings > /dev/null 2>&1; then
echo "QuestDB is ready"
break
fi
echo "Waiting for QuestDB... ($i/60)"
sleep 1
done
displayName: 'Install QuestDB via systemd (Linux)'
condition: eq(variables['osName'], 'Linux')

- script: |
brew install questdb
questdb start
# Wait for QuestDB to be ready
for i in {1..60}; do
if curl -s http://localhost:9000/settings > /dev/null 2>&1; then
echo "QuestDB is ready"
break
fi
echo "Waiting for QuestDB... ($i/60)"
sleep 1
done
displayName: 'Install QuestDB via Homebrew (macOS)'
condition: eq(variables['osName'], 'macOS')

- pwsh: |
Write-Host "Downloading QuestDB..."
$downloadUrl = "https://github.com/questdb/questdb/releases/download/9.2.2/questdb-9.2.2-rt-windows-x86-64.tar.gz"
Invoke-WebRequest -Uri $downloadUrl -OutFile questdb.tar.gz

Write-Host "Extracting QuestDB..."
tar -xzf questdb.tar.gz

Write-Host "Listing extracted contents..."
Get-ChildItem

Write-Host "Starting QuestDB..."
$questdbExe = Get-ChildItem -Recurse -Filter "questdb.exe" | Select-Object -First 1
if ($questdbExe) {
Write-Host "Found questdb.exe at: $($questdbExe.FullName)"
& $questdbExe.FullName start
} else {
Write-Host "ERROR: questdb.exe not found after extraction"
exit 1
}

# Wait for QuestDB to be ready (up to 120 seconds)
Write-Host "Waiting for QuestDB to start..."
$maxAttempts = 120
for ($i = 1; $i -le $maxAttempts; $i++) {
try {
$response = Invoke-WebRequest -Uri "http://localhost:9000/settings" -UseBasicParsing -ErrorAction SilentlyContinue
if ($response.StatusCode -eq 200) {
Write-Host "QuestDB is ready"
break
}
} catch {
# Silently continue
}
if ($i % 30 -eq 0) {
Write-Host "Still waiting... ($i/$maxAttempts seconds)"
}
Start-Sleep -Seconds 1
}
displayName: 'Install and Start QuestDB (Windows)'
condition: eq(variables['osName'], 'Windows')

- task: DotNetCoreCLI@2
displayName: 'Run tests on $(osName)'
inputs:
Expand Down
54 changes: 41 additions & 13 deletions src/dummy-http-server/DummyHttpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public class DummyHttpServer : IDisposable
private readonly WebApplication _app;
private int _port = 29743;
private readonly TimeSpan? _withStartDelay;
private readonly bool _withTokenAuth;
private readonly bool _withBasicAuth;
private readonly bool _withRetriableError;
private readonly bool _withErrorMessage;
private readonly bool _requireClientCert;

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

// Store configuration in instance fields instead of static fields
// to avoid interference between multiple concurrent servers
_withTokenAuth = withTokenAuth;
_withBasicAuth = withBasicAuth;
_withRetriableError = withRetriableError;
_withErrorMessage = withErrorMessage;
_withStartDelay = withStartDelay;
_requireClientCert = requireClientCert;

// Also set static flags for backwards compatibility
IlpEndpoint.WithTokenAuth = withTokenAuth;
IlpEndpoint.WithBasicAuth = withBasicAuth;
IlpEndpoint.WithRetriableError = withRetriableError;
IlpEndpoint.WithErrorMessage = withErrorMessage;
_withStartDelay = withStartDelay;

if (withTokenAuth)
{
Expand All @@ -91,16 +105,22 @@ public DummyHttpServer(bool withTokenAuth = false, bool withBasicAuth = false, b
}

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

_app = bld.Build();

_app.MapHealthChecks("/ping");
_app.UseDefaultExceptionHandler();

// Add middleware to set X-Server-Port header so endpoints know which port they're running on
_app.Use(async (context, next) =>
{
context.Request.Headers["X-Server-Port"] = _port.ToString();
await next();
});

if (withTokenAuth)
{
_app
Expand All @@ -126,10 +146,7 @@ public void Dispose()
/// </remarks>
public void Clear()
{
IlpEndpoint.ReceiveBuffer.Clear();
IlpEndpoint.ReceiveBytes.Clear();
IlpEndpoint.LastError = null;
IlpEndpoint.Counter = 0;
IlpEndpoint.ClearPort(_port);
}

/// <summary>
Expand All @@ -148,7 +165,18 @@ public async Task StartAsync(int port = 29743, int[]? versions = null)
versions ??= new[] { 1, 2, 3, };
SettingsEndpoint.Versions = versions;
_port = port;
_ = _app.RunAsync($"http://localhost:{port}");

// Store configuration flags keyed by port so multiple servers don't interfere
IlpEndpoint.SetPortConfig(port,
tokenAuth: _withTokenAuth,
basicAuth: _withBasicAuth,
retriableError: _withRetriableError,
errorMessage: _withErrorMessage);

var url = _requireClientCert
? $"https://localhost:{port}"
: $"http://localhost:{port}";
_ = _app.RunAsync(url);
}

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

/// <summary>
Expand All @@ -179,12 +207,12 @@ public StringBuilder GetReceiveBuffer()
/// <returns>The mutable list of bytes received by the endpoint.</returns>
public List<byte> GetReceivedBytes()
{
return IlpEndpoint.ReceiveBytes;
return IlpEndpoint.GetReceiveBytes(_port);
}

public Exception? GetLastError()
{
return IlpEndpoint.LastError;
return IlpEndpoint.GetLastError(_port);
}

public async Task<bool> Healthcheck()
Expand Down Expand Up @@ -215,7 +243,7 @@ public async Task<bool> Healthcheck()

public int GetCounter()
{
return IlpEndpoint.Counter;
return IlpEndpoint.GetCounter(_port);
}

/// <summary>
Expand Down
142 changes: 132 additions & 10 deletions src/dummy-http-server/IlpEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,117 @@ public class IlpEndpoint : Endpoint<Request, JsonErrorResponse?>
{
private const string Username = "admin";
private const string Password = "quest";
public static readonly StringBuilder ReceiveBuffer = new();
public static readonly List<byte> ReceiveBytes = new();
public static Exception? LastError = new();

// Port-keyed storage to support multiple concurrent DummyHttpServer instances
private static readonly Dictionary<int, (StringBuilder Buffer, List<byte> Bytes, Exception? Error, int Counter)>
PortData = new();

// Port-keyed configuration to support multiple concurrent DummyHttpServer instances
private static readonly Dictionary<int, (bool TokenAuth, bool BasicAuth, bool RetriableError, bool ErrorMessage)>
PortConfig = new();

// Configuration flags (global, apply to all servers) - kept for backwards compatibility
public static bool WithTokenAuth = false;
public static bool WithBasicAuth = false;
public static bool WithRetriableError = false;
public static bool WithErrorMessage = false;
public static int Counter;

// Get the port from request headers (set by DummyHttpServer)
private static int GetPortKey(HttpContext context)
{
if (context?.Request.Headers.TryGetValue("X-Server-Port", out var portHeader) == true
&& int.TryParse(portHeader.ToString(), out var port))
{
return port;
}
return context?.Connection?.LocalPort ?? 0;
}

private static (StringBuilder Buffer, List<byte> Bytes, Exception? Error, int Counter) GetOrCreatePortData(int port)
{
lock (PortData)
{
if (!PortData.TryGetValue(port, out var data))
{
data = (new StringBuilder(), new List<byte>(), null, 0);
PortData[port] = data;
}
return data;
}
}


// Public methods for accessing port-specific data (used by DummyHttpServer)
public static StringBuilder GetReceiveBuffer(int port) => GetOrCreatePortData(port).Buffer;
public static List<byte> GetReceiveBytes(int port) => GetOrCreatePortData(port).Bytes;

public static Exception? GetLastError(int port)
{
lock (PortData)
{
return GetOrCreatePortData(port).Error;
}
}

public static void SetLastError(int port, Exception? error)
{
lock (PortData)
{
var data = GetOrCreatePortData(port);
PortData[port] = (data.Buffer, data.Bytes, error, data.Counter);
}
}

public static int GetCounter(int port)
{
lock (PortData)
{
return GetOrCreatePortData(port).Counter;
}
}

public static void SetCounter(int port, int value)
{
lock (PortData)
{
var data = GetOrCreatePortData(port);
PortData[port] = (data.Buffer, data.Bytes, data.Error, value);
}
}

public static void ClearPort(int port)
{
lock (PortData)
{
if (PortData.TryGetValue(port, out var data))
{
data.Buffer.Clear();
data.Bytes.Clear();
PortData[port] = (data.Buffer, data.Bytes, null, 0);
}
}
}

public static void SetPortConfig(int port, bool tokenAuth, bool basicAuth, bool retriableError, bool errorMessage)
{
lock (PortConfig)
{
PortConfig[port] = (tokenAuth, basicAuth, retriableError, errorMessage);
}
}

private static (bool TokenAuth, bool BasicAuth, bool RetriableError, bool ErrorMessage) GetPortConfig(int port)
{
lock (PortConfig)
{
if (PortConfig.TryGetValue(port, out var config))
{
return config;
}
// Return static flags as defaults for backwards compatibility
return (WithTokenAuth, WithBasicAuth, WithRetriableError, WithErrorMessage);
}
}

public override void Configure()
{
Expand All @@ -111,14 +214,24 @@ public override void Configure()

public override async Task HandleAsync(Request req, CancellationToken ct)
{
Counter++;
if (WithRetriableError)
int port = GetPortKey(HttpContext);
var data = GetOrCreatePortData(port);
var config = GetPortConfig(port);

lock (PortData)
{
// Increment counter for this port
data = GetOrCreatePortData(port);
PortData[port] = (data.Buffer, data.Bytes, data.Error, data.Counter + 1);
}

if (config.RetriableError)
{
await SendAsync(null, 500, ct);
return;
}

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

try
{
ReceiveBuffer.Append(req.StringContent);
ReceiveBytes.AddRange(req.ByteContent);
lock (PortData)
{
data = GetOrCreatePortData(port);
data.Buffer.Append(req.StringContent);
data.Bytes.AddRange(req.ByteContent);
PortData[port] = data;
}
await SendNoContentAsync(ct);
}
catch (Exception ex)
{
LastError = ex;
lock (PortData)
{
data = GetOrCreatePortData(port);
PortData[port] = (data.Buffer, data.Bytes, ex, data.Counter);
}
throw;
}
}
Expand Down
Loading