Skip to content

Commit 5ffdd25

Browse files
committed
v5.3.3: NOMINMAX guards, thread safety documentation (Issue #24)
- Add #ifndef NOMINMAX guards to live_client_wrapper.cpp and live_blocking_wrapper.cpp preventing <windows.h> min/max macro conflicts (Issue #24) - Add thread safety documentation to ILiveClient and ILiveBlockingClient interfaces documenting databento-cpp calling convention - Add thread safety callout to README - Add concurrent Subscribe pitfall to coding agent guide - Create future_improvements.md for tracked polish items
1 parent f0538cb commit 5ffdd25

9 files changed

Lines changed: 107 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable changes to databento-dotnet will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [5.3.3] - 2026-05-29
9+
10+
### Fixed
11+
12+
- **Add NOMINMAX guards to native wrappers** — prevents `<windows.h>` min/max macro conflicts that could cause compilation failures with certain build configurations (Issue #24)
13+
14+
### Changed
15+
16+
- **Thread safety documentation** — added class-level and method-level thread safety remarks to `ILiveClient` and `ILiveBlockingClient` documenting the databento-cpp calling convention: complete all Subscribe calls before StartAsync, await each call sequentially
17+
- **README thread safety callout** — added thread safety guidance between LiveClient and LiveBlockingClient API reference sections
18+
- **Coding agent guide** — updated to v5.3.3, added pitfall #6 documenting concurrent Subscribe call risks with correct/incorrect code examples
19+
820
## [5.3.2] - 2026-05-26
921

1022
### Fixed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# databento-dotnet
22

3-
[![NuGet](https://img.shields.io/badge/NuGet-v5.3.2-blue)](https://www.nuget.org/packages/Databento.Client)
3+
[![NuGet](https://img.shields.io/badge/NuGet-v5.3.3-blue)](https://www.nuget.org/packages/Databento.Client)
44
[![Downloads](https://img.shields.io/badge/Downloads-18.4K-blue)](https://www.nuget.org/packages/Databento.Client)
55
[![.NET](https://img.shields.io/badge/.NET-8.0%20%7C%209.0-purple)](https://dotnet.microsoft.com/)
66
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
@@ -329,6 +329,8 @@ client.DataReceived += (s, e) => { };
329329
client.ErrorOccurred += (s, e) => { };
330330
```
331331

332+
> **Thread safety:** `SubscribeAsync` is not thread-safe. Always `await` each call sequentially and complete all subscriptions before calling `StartAsync`. The `await` pattern shown above naturally serializes calls and is safe by construction. This mirrors the databento-cpp calling convention.
333+
332334
### LiveBlockingClient
333335

334336
Pull-based API for explicit control over record retrieval:

Readme_For_Coding_Agents.md

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

33
> **For AI coding agents**: This document is optimized for programmatic consumption by agentic code tools (Claude Code CLI, Cursor, GitHub Copilot Workspace, etc.). Use this as your primary reference when working with the Databento.Client library.
44
5-
**Library**: `Databento.Client` v5.1.4
5+
**Library**: `Databento.Client` v5.3.3
66
**Package**: `dotnet add package Databento.Client`
77
**Runtime**: .NET 8.0 / 9.0
88
**Platforms**: Windows x64 (NuGet) | Linux/macOS (build from source)
@@ -450,6 +450,20 @@ await client.StartAsync();
450450
await foreach (var record in client.StreamAsync()) { }
451451
```
452452

453+
### 6. Concurrent Subscribe Calls (Thread Safety)
454+
```csharp
455+
// WRONG - Data race in native layer (unsynchronized socket writes and std::vector mutation)
456+
var t1 = Task.Run(() => client.SubscribeAsync("GLBX.MDP3", Schema.Mbp1, new[] { "ESM5" }));
457+
var t2 = Task.Run(() => client.SubscribeAsync("GLBX.MDP3", Schema.Trades, new[] { "NQM5" }));
458+
await Task.WhenAll(t1, t2);
459+
460+
// CORRECT - Await each call sequentially, then start
461+
await client.SubscribeAsync("GLBX.MDP3", Schema.Mbp1, new[] { "ESM5" });
462+
await client.SubscribeAsync("GLBX.MDP3", Schema.Trades, new[] { "NQM5" });
463+
await client.StartAsync();
464+
```
465+
`SubscribeAsync` is not thread-safe. Using `await` on each call naturally serializes them and mitigates this risk. This mirrors the databento-cpp calling convention where thread safety is by design (single-threaded setup), not by enforcement (no internal locks).
466+
453467
---
454468

455469
## Method Signatures Quick Reference

future_improvements.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Future Improvements
2+
3+
## Polish
4+
5+
### Safe (non-breaking)
6+
7+
**1. Add logging to `SubscribeWithSnapshotAsync` in `LiveClient.cs`**
8+
- File: `src/Databento.Client/Live/LiveClient.cs` (lines ~327-370)
9+
- `SubscribeAsync` logs on entry and success. `SubscribeWithSnapshotAsync` has zero logging.
10+
- Fix: Add `_logger.LogInformation(...)` for entry and success to match `SubscribeAsync` pattern.
11+
12+
**2. Inline unnecessary local variable in `BlockUntilStoppedAsync` in `LiveClient.cs`**
13+
- File: `src/Databento.Client/Live/LiveClient.cs` (line ~880)
14+
- `var streamTask = Interlocked.CompareExchange(ref _streamTask, null, null);` assigns to a variable only used for a null check.
15+
- Fix: Change to `if (Interlocked.CompareExchange(ref _streamTask, null, null) == null)`.
16+
- Note: Same pattern exists in the `BlockUntilStoppedAsync(TimeSpan timeout, ...)` overload.
17+
18+
### Potentially Breaking
19+
20+
**3. Add `ConfigureAwait(false)` to `StreamAsync` in `LiveClient.cs`**
21+
- File: `src/Databento.Client/Live/LiveClient.cs` (line ~615)
22+
- `await foreach (var record in _recordChannel.Reader.ReadAllAsync(cancellationToken))` is missing `.ConfigureAwait(false)`.
23+
- Standard best practice for library code — not having it can cause deadlocks in environments with a `SynchronizationContext`.
24+
- **Why breaking:** Changes thread affinity for callers using the library in WPF, WinForms, or ASP.NET apps with a `SynchronizationContext`. Their `await foreach` would resume on a thread pool thread instead of their original context. We don't know all use cases of this library.
25+
26+
**4. Remove `async`/`await Task.CompletedTask` from `ResubscribeAsync` in `LiveClient.cs`**
27+
- File: `src/Databento.Client/Live/LiveClient.cs` (lines ~568-586)
28+
- Method is marked `async` and ends with `await Task.CompletedTask`. The `async` keyword generates a state machine for no reason — the method is entirely synchronous (P/Invoke call).
29+
- Correct pattern: remove `async`, return `Task.CompletedTask` (same as `SubscribeAsync` does).
30+
- **Why breaking:** With `async`, exceptions from the P/Invoke are captured into the returned `Task` and thrown at the `await` site. Without `async`, exceptions throw synchronously at the call site. For callers who `await` immediately, no difference. For callers who capture the `Task` and `await` later, the exception timing changes.
31+
32+
**5. Replace `ConcurrentBag` with `List` for `_subscriptions` in `LiveClient.cs`**
33+
- File: `src/Databento.Client/Live/LiveClient.cs` (line ~38)
34+
- `ConcurrentBag` was a defensive choice. All writes (`_subscriptions.Add`) happen during `SubscribeAsync` which is synchronous and completes before `StartAsync`. After `Start`, the collection is read-only. `List` is safe for concurrent reads with no concurrent writes.
35+
- databento-cpp uses a plain `std::vector``ConcurrentBag` is a fidelity divergence.
36+
- The comment "HIGH FIX: Use thread-safe collection for concurrent subscription operations" was added during a code quality pass but the concurrent write scenario doesn't materialize in the current code. `ResubscribeAsync` uses native resubscribe (`dbento_live_resubscribe`), not the managed collection.
37+
- `ConcurrentBag` is on the cold setup path (not the hot data path) and has zero overhead when uncontended, so it's harmless. It differs from `SemaphoreSlim` (which would serialize P/Invoke operations on the hot path).
38+
- **Why breaking:** If a code path exists that we haven't identified where a background thread accesses `_subscriptions`, replacing with `List` introduces a race condition. Requires thorough analysis and testing before changing.
39+
40+
### Cosmetic (low priority)
41+
42+
**6. Shorten `Models.Dbn.DbnMetadata` references in `LiveClient.cs`**
43+
- File: `src/Databento.Client/Live/LiveClient.cs`
44+
- Fully qualified `Models.Dbn.DbnMetadata` is used throughout. Could add a `using` directive and use `DbnMetadata`.
45+
- Purely cosmetic, zero runtime impact. Not worth the churn unless touching the file for other reasons.

src/Databento.Client/Databento.Client.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
<!-- Package Information -->
1111
<PackageId>Databento.Client</PackageId>
12-
<Version>5.3.2</Version>
12+
<Version>5.3.3</Version>
1313
<Authors>Alparse</Authors>
1414
<Company>Databento</Company>
1515
<Product>databento-dotnet</Product>
@@ -20,7 +20,7 @@
2020
<RepositoryType>git</RepositoryType>
2121
<PackageProjectUrl>https://github.com/Alparse/databento-dotnet</PackageProjectUrl>
2222
<PackageReadmeFile>README.md</PackageReadmeFile>
23-
<PackageReleaseNotes>v5.3.1: Fix Record.FromBytes failing when ts_out is enabled — records now deserialize correctly with the extra 8-byte gateway timestamp. Added TsOutNs and TsOut properties to expose the gateway send timestamp (Issue #23).</PackageReleaseNotes>
23+
<PackageReleaseNotes>v5.3.3: Add NOMINMAX guards to native wrappers fixing min/max macro conflicts (Issue #24). Add thread safety documentation to ILiveClient, ILiveBlockingClient, README, and coding agent guide.</PackageReleaseNotes>
2424
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
2525

2626
<!-- XML Documentation -->

src/Databento.Client/Live/ILiveBlockingClient.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,27 @@ namespace Databento.Client.Live;
88
/// Provides synchronous control over record retrieval via StartAsync/NextRecordAsync.
99
/// </summary>
1010
/// <remarks>
11+
/// <para>
1112
/// This is the pull-based API counterpart to LiveClient (LiveThreaded).
1213
/// Use this when you want explicit control over when records are retrieved,
1314
/// as opposed to push-based event/callback delivery.
15+
/// </para>
16+
/// <para>
17+
/// <b>Thread safety:</b> All Subscribe calls must complete before calling <see cref="StartAsync"/>.
18+
/// Subscribe methods are not thread-safe — do not call them concurrently or after the stream has started.
19+
/// Using <c>await</c> on each call naturally serializes them and mitigates this risk.
20+
/// This mirrors the databento-cpp LiveBlocking calling convention where the Subscribe path performs
21+
/// unsynchronized socket writes.
22+
/// </para>
1423
/// </remarks>
1524
public interface ILiveBlockingClient : IAsyncDisposable
1625
{
1726
/// <summary>
1827
/// Subscribe to a dataset and schema
1928
/// </summary>
29+
/// <remarks>
30+
/// Not thread-safe. Await each call and complete all subscriptions before calling <see cref="StartAsync"/>.
31+
/// </remarks>
2032
/// <param name="dataset">Dataset to subscribe to (e.g., "EQUS.MINI")</param>
2133
/// <param name="schema">Schema type to receive</param>
2234
/// <param name="symbols">List of symbols to subscribe to</param>

src/Databento.Client/Live/ILiveClient.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ namespace Databento.Client.Live;
66
/// <summary>
77
/// Live streaming client for real-time market data
88
/// </summary>
9+
/// <remarks>
10+
/// <b>Thread safety:</b> All Subscribe calls must complete before calling <see cref="StartAsync"/>.
11+
/// Subscribe methods are not thread-safe — do not call them concurrently or after the stream has started.
12+
/// Using <c>await</c> on each call naturally serializes them and mitigates this risk.
13+
/// This mirrors the databento-cpp calling convention where thread safety is by design (single-threaded setup),
14+
/// not by enforcement (no internal locks).
15+
/// </remarks>
916
public interface ILiveClient : IAsyncDisposable
1017
{
1118
/// <summary>
@@ -21,6 +28,9 @@ public interface ILiveClient : IAsyncDisposable
2128
/// <summary>
2229
/// Subscribe to a data stream (matches databento-cpp Subscribe overloads)
2330
/// </summary>
31+
/// <remarks>
32+
/// Not thread-safe. Await each call and complete all subscriptions before calling <see cref="StartAsync"/>.
33+
/// </remarks>
2434
/// <param name="dataset">Dataset name (e.g., "GLBX.MDP3")</param>
2535
/// <param name="schema">Schema type</param>
2636
/// <param name="symbols">List of symbols to subscribe to</param>

src/Databento.Native/src/live_blocking_wrapper.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
#define NOMINMAX // Prevent Windows min/max macros from interfering with std::numeric_limits
1+
#ifndef NOMINMAX
2+
# define NOMINMAX
3+
#endif
4+
25
#include "databento_native.h"
36
#include "common_helpers.hpp"
47
#include "handle_validation.hpp"

src/Databento.Native/src/live_client_wrapper.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
#ifndef NOMINMAX
2+
# define NOMINMAX
3+
#endif
4+
15
#include "databento_native.h"
26
#include "common_helpers.hpp"
37
#include "handle_validation.hpp"

0 commit comments

Comments
 (0)