Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
119 commits
Select commit Hold shift + click to select a range
0697175
support QWIP and sf for .net client
kafka1991 Apr 28, 2026
57e103b
code review and add cancelrow
kafka1991 Apr 29, 2026
30f1266
Merge branch 'main' into qwip_victor
kafka1991 Apr 29, 2026
5db8e98
fix linux test failed
kafka1991 Apr 29, 2026
98bdf17
code review and fix fsn race condition
kafka1991 Apr 29, 2026
b7abb47
code review and race condition
kafka1991 Apr 29, 2026
a9c5b9e
add bit writer tests
kafka1991 Apr 29, 2026
c56ffbb
flake tests
kafka1991 Apr 29, 2026
fd615a9
code review
kafka1991 Apr 29, 2026
b1e2b69
code review
kafka1991 Apr 30, 2026
c9bd822
code review and fix tests
kafka1991 Apr 30, 2026
3e70218
code review
kafka1991 Apr 30, 2026
334f521
code review
kafka1991 Apr 30, 2026
c4c1994
add more tests
kafka1991 Apr 30, 2026
4724fbc
update benchmark
kafka1991 Apr 30, 2026
a0a7f10
update benchmark docs
kafka1991 Apr 30, 2026
3f3c9eb
same seal file name
kafka1991 Apr 30, 2026
8eb4241
add egress
kafka1991 Apr 30, 2026
9809362
fix integration tests
kafka1991 Apr 30, 2026
442b108
code review
kafka1991 Apr 30, 2026
84d1185
code review
kafka1991 Apr 30, 2026
204c98f
code review
kafka1991 May 1, 2026
c3bd641
add qwip_victor branch review + actionable plan
nwoolmer May 5, 2026
1c8ee2b
1. ingress support configure multi servers 2. egress support random s…
kafka1991 May 6, 2026
cc07a95
optimise failover
kafka1991 May 6, 2026
22b6f0c
Merge pull request #75 from questdb/egress
kafka1991 May 6, 2026
baa6c6d
code review
kafka1991 May 6, 2026
e15f54c
code review and ci failed
kafka1991 May 6, 2026
3a81f6a
ci add timeout
kafka1991 May 6, 2026
78f2738
ci fix
kafka1991 May 6, 2026
3b135e8
code review
kafka1991 May 6, 2026
072aae4
code review
kafka1991 May 6, 2026
2729f94
code review
kafka1991 May 6, 2026
cf0510d
code review and fix bugs
kafka1991 May 6, 2026
360c01d
code review
kafka1991 May 6, 2026
9e4d4da
code review
kafka1991 May 6, 2026
a3f5301
add initial_connect_retry argument
kafka1991 May 7, 2026
2d5f824
fix race condition
kafka1991 May 7, 2026
1f6efad
fix failed tests
kafka1991 May 7, 2026
6b91ead
final code review
kafka1991 May 7, 2026
315fb9a
code review and remove obselete tests
kafka1991 May 8, 2026
48f6cdb
make tests stable
kafka1991 May 8, 2026
b0707e4
alian specs
kafka1991 May 9, 2026
011490e
code review and fix one race condition
kafka1991 May 11, 2026
5f531ca
add integration tests for qwp protocol
kafka1991 May 11, 2026
4f99ac2
fix durable model and fix integration tests
kafka1991 May 11, 2026
4d183e5
add more integration tests
kafka1991 May 11, 2026
02be36c
fix failed tests
kafka1991 May 11, 2026
01ad7ac
fix failed tests
kafka1991 May 11, 2026
3edd120
add options.durable_ack_keepalive_interval_millis config
kafka1991 May 12, 2026
3fdb773
little code review
kafka1991 May 12, 2026
6a450c0
finally code review
kafka1991 May 12, 2026
3b75281
code review
kafka1991 May 12, 2026
a545528
add ackwatermark
kafka1991 May 12, 2026
f8f7e4d
code review
kafka1991 May 12, 2026
5cbabc5
add listenser
kafka1991 May 13, 2026
bbba3c5
fix spec drift
kafka1991 May 13, 2026
d3fcfea
fix slot lock race condition
kafka1991 May 13, 2026
d859137
code review
kafka1991 May 13, 2026
a1d5f68
fix race condition
kafka1991 May 13, 2026
cfa9b0c
final code review
kafka1991 May 14, 2026
6662cc2
add more tests
kafka1991 May 14, 2026
fb94dc3
final code review
kafka1991 May 14, 2026
efad6e0
fix decimal scale
kafka1991 May 15, 2026
be94782
add fuzz and benchmark and fix serval bugs
kafka1991 May 18, 2026
834f906
CI use jdk25
kafka1991 May 18, 2026
534706a
code review and add more fuzz tests
kafka1991 May 18, 2026
d945531
add failover tests
kafka1991 May 18, 2026
3839119
add failover tests
kafka1991 May 18, 2026
3221e41
fix connstring and ci
kafka1991 May 18, 2026
a888361
revert some fixes
kafka1991 May 18, 2026
8ce1e9c
fix connect str and graceful shutdown QuestDb server
kafka1991 May 18, 2026
6f7801c
adjust rowcount
kafka1991 May 18, 2026
bf66b39
adjust rowcount
kafka1991 May 18, 2026
d5fb821
adjust tiemout on CI
kafka1991 May 18, 2026
11761f4
add more fuzz tests
kafka1991 May 18, 2026
ad87da3
another round reviews and add more tests
kafka1991 May 18, 2026
449b1fb
connect string should work for both ingress and egress
kafka1991 May 19, 2026
ea3fba7
code reviews
kafka1991 May 19, 2026
f072fda
falke test on macos
kafka1991 May 19, 2026
169e547
add memory align issue comment
kafka1991 May 20, 2026
4585636
add Enterprise e2e integration test infrastructure
ideoma May 26, 2026
3348de2
ent pipeline sidecar fixes
ideoma May 26, 2026
075d314
add enterprise e2e dispatch to PR pipeline
ideoma May 27, 2026
b51d261
move e2e build trigger to happen earlier
ideoma May 27, 2026
58d28e1
inline schema and remove schema registry
kafka1991 Jun 16, 2026
ffbb5f7
add server start vm option `-add-exports=java.base/jdk.internal.vm=io…
kafka1991 Jun 17, 2026
ce087d6
update questdb server's java client
kafka1991 Jun 17, 2026
056d0de
Remove the auth config parameter
mtopolnik Jun 18, 2026
6217794
Remove the gorilla ingress parameter
mtopolnik Jun 18, 2026
2bb9f2b
Reject non-QWP keys on ws:: connect strings
mtopolnik Jun 18, 2026
a122152
Fix stale QWP encoder test vectors for always-on Gorilla
mtopolnik Jun 18, 2026
433b7ea
Accept full QWP key union, reject non-QWP keys
mtopolnik Jun 18, 2026
55ab35c
add convert_local_to_utc config to conditionally enable local to utc …
ideoma Jun 19, 2026
2a5793c
fix: QwpMemorySegment has no finalizer → native-memory leak on the de…
ideoma Jun 22, 2026
8e8374b
fix: use enum instead of byte consts
ideoma Jun 22, 2026
38b1301
fix compilation
ideoma Jun 22, 2026
3061ac4
fix: Cancel() races on the shared _cancelRequested flag and can cance…
ideoma Jun 23, 2026
a66fbcd
fix: make QwpDecodeException public
ideoma Jun 23, 2026
30a1df1
fix minor reveiw points
ideoma Jun 23, 2026
5252feb
refactor decimals to force choosing type, precision upfront
ideoma Jun 23, 2026
3630f74
add ColumnDecimalXX method to ISender. Fix tests
ideoma Jun 24, 2026
b631099
fix: close_flush_timeout_millis=0 silently drops buffered rows on dis…
ideoma Jun 24, 2026
775ec81
fix: Malformed varint in RESULT_END/EXEC_DONE is misclassified as ret…
ideoma Jun 24, 2026
a613a9d
fix: docs: SenderOptions.ToString() does not re-parse for any authent…
ideoma Jun 24, 2026
2197f67
fix: Worst-case synchronous Dispose() latency compounds
ideoma Jun 24, 2026
3040de4
fix: macOS SF hot-spare files are never block-reserved → SIGBUS proc…
ideoma Jun 24, 2026
32d8f00
fix: Lowering sf_max_bytes between restarts silently truncates replay…
ideoma Jun 24, 2026
8447962
fix: QwpColumnBatch numeric accessors silently return wrong data on a…
ideoma Jun 25, 2026
e1fd4be
fix: Egress array accessors silently return wrong-typed data on a mis…
ideoma Jun 25, 2026
41b0ed9
fix: TLS CA certificate loaded for WS sender / query client is never …
ideoma Jun 25, 2026
7e868ca
fix: Infinite loop (process hang) in QwpColumn.GrowTo after Truncate()
ideoma Jun 25, 2026
d3d2a03
fix: target / initial_credit silently accepted (and ignored) on HTTP/…
ideoma Jun 25, 2026
c5b9463
fix: Unbounded reconnect backoff can throw from Task.Delay
ideoma Jun 25, 2026
b29e28f
fix: Ingest never records a host's zone on a successful connect
ideoma Jun 25, 2026
72f83e3
nits
ideoma Jun 26, 2026
6993c60
fix: A lone-surrogate symbol value permanently wedges the WS sender (…
ideoma Jun 26, 2026
cbd6fed
feat: implement transactions
ideoma Jun 26, 2026
66c87ad
fix: Transient drain timeout permanently abandons recoverable orphan …
ideoma Jun 26, 2026
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
354 changes: 354 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

148 changes: 146 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,127 @@ using var sender = Sender.New("https::addr=localhost:9009;tls_verify=unsafe_off;
using var sender = Sender.New("tcps::addr=localhost:9009;tls_verify=unsafe_off;username=admin;token=NgdiOWDoQNUP18WOnb1xkkEG5TzPYMda5SiUOvT1K0U=;");
```

### WebSocket / QWP (columnar binary, requires .NET 7+)

The `ws::` and `wss::` schemes use the QuestDB columnar binary protocol (QWP) over a WebSocket. Compared to `http::` / `tcp::` (text ILP), QWP delivers higher sustained throughput at lower CPU cost — payloads are smaller because columns share schema once per connection.

```csharp
using var sender = Sender.New("ws::addr=localhost:9000;");
sender.Table("trades")
.Symbol("symbol", "ETH-USD")
.Column("price", 2615.54)
.Column("amount", 0.00044)
.At(DateTime.UtcNow);
sender.Send();
```

`wss::` adds TLS:

```csharp
using var sender = Sender.New("wss::addr=q.example.com:443;username=admin;password=quest;");
```

#### Pipelined async mode

By default the WebSocket sender pipelines up to 128 batches in flight. Use the `*Async` API to keep the calling thread free while frames are on the wire:

```csharp
await using var sender = Sender.New("ws::addr=localhost:9000;");

for (var i = 0; i < 1_000_000; i++)
{
sender.Table("trades")
.Symbol("symbol", "ETH-USD")
.Column("price", 2615.54);
await sender.AtAsync(DateTime.UtcNow);
}

await sender.SendAsync();
```

`in_flight_window` controls the pipeline depth; valid range is `2..N`. The WebSocket transport is async-only — `in_flight_window=1` is rejected.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

#### Examples

Working sample projects (drop-in copies):

- [`src/example-websocket`](src/example-websocket/Program.cs) — minimal `ws::` sender.
- [`src/example-websocket-auth-tls`](src/example-websocket-auth-tls/Program.cs) — `wss::` with Basic auth and a custom TLS root.

Run with `dotnet run --project src/example-websocket`.

#### Gorilla timestamp compression

Set `gorilla=on` to enable delta-of-delta compression for timestamp columns. Best fit for steady-tick streams (sensor readings, evenly spaced ticks). The encoder transparently falls back to uncompressed per column when DoDs overflow int32:

```csharp
using var sender = Sender.New("ws::addr=localhost:9000;gorilla=on;");
```

For irregular timestamps (event-driven workloads) Gorilla can be larger than uncompressed; benchmark with your actual data before enabling.

#### Durable acknowledgements

Set `request_durable_ack=on` to opt into per-table object-store watermarks. The sender exposes them via the `IQwpWebSocketSender` interface:

```csharp
using var sender = Sender.New("ws::addr=localhost:9000;request_durable_ack=on;");
sender.Table("trades").Column("v", 1L).At(DateTime.UtcNow);
sender.Send();

if (sender is IQwpWebSocketSender ws)
{
long committed = ws.GetHighestAckedSeqTxn("trades"); // -1 if none yet
long durable = ws.GetHighestDurableSeqTxn("trades"); // requires the opt-in
ws.Ping(); // wait for in-flight to drain
}
```

#### Defaults

| Knob | WebSocket default | HTTP / TCP for comparison |
|-------------------------------|-------------------|---------------------------------|
| Default port | 9000 | 9000 (HTTP), 9009 (TCP) |
| Endpoint path | `/write/v4` | `/write` (HTTP) |
| `auto_flush_rows` | 1000 | 75000 (HTTP), 600 (TCP) |
| `auto_flush_interval` | 100 ms | 1000 ms |
| `auto_flush_bytes` | `int.MaxValue` | `int.MaxValue` |
| `in_flight_window` | 128 | n/a |
| `close_timeout` | 5000 ms | n/a |
| `max_schemas_per_connection` | 65535 | n/a |
| `request_durable_ack` | `off` | n/a |
| `gorilla` | `off` | n/a |

#### Store-and-forward (durable client buffer)

Set `sf_dir=/path/to/dir` to opt into the on-disk store-and-forward buffer. Outgoing batches are persisted to mmap'd segments before going on the wire, and a background I/O thread silently reconnects + replays whatever's still on disk if the network drops or the process restarts. User code is shielded from transient disconnects; a `Send` can still surface terminal errors when the bounded retry / drain budgets (`sf_append_deadline_millis`, `reconnect_max_duration_millis`) expire.

```csharp
using var sender = Sender.New(
"ws::addr=localhost:9000;sf_dir=/var/lib/myapp/qwp;sender_id=ingester-01;");
```

Each sender owns one slot directory at `<sf_dir>/<sender_id>/`. `sender_id` defaults to `"default"` and **must be unique per process** sharing the same `sf_dir`. To reclaim slots left by a crashed sibling process, set `drain_orphans=on`:

```csharp
using var sender = Sender.New(
"ws::addr=localhost:9000;sf_dir=/var/lib/myapp/qwp;sender_id=ingester-01;drain_orphans=on;");
```

SF caveats:

- **Local filesystem only.** `FileShare.None` advisory locking does not behave reliably on NFS or other networked filesystems. Point `sf_dir` at a local disk.
- **SF frames are larger.** The sender uses self-sufficient encoding (every frame carries the full schema + symbol dictionary) so any frame can be replayed against a fresh server connection. Expect somewhat larger payload-per-batch vs non-SF mode.
- Only `sf_durability=memory` is supported in v1 (matches Java).

#### Caveats

- **`ws::` / `wss::` requires .NET 7 or later.** HTTP and TCP transports keep working on net6.0.
- The transport disables system HTTP proxies by default; long-lived WebSocket connections rarely survive HTTP proxies. Pass an explicit `IWebProxy` to override if you have a WebSocket-aware proxy.
- Multi-address `addr=h1,h2` is **not** supported on the WebSocket transport.
- **Use long-lived senders.** WebSocket upgrade is significantly more expensive than an HTTP POST; create the sender once at startup and keep it alive for the process lifetime, rather than per request.
- **Connect-string quoting differs from Java/Go.** This client parses connect strings via `System.Data.Common.DbConnectionStringBuilder`, which uses ADO.NET-style `'`/`"` quoting with internal doubling. Java and Go implement `;;` → `;` escaping. A connect string with a literal semicolon in a value (rare; mostly passwords or paths) parses differently across clients — quote the value or escape per the local parser.

### Multiple database endpoints

The client can be configured with multiple `addr` entries pointing to different instances of QuestDB.
Expand All @@ -147,8 +268,8 @@ The config string format is:

| Name | Default | Description |
| ------------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `protocol` (schema) | `http` | The transport protocol to use. Options are http(s)/tcp(s). |
| `addr` | `localhost:9000` | The {host}:{port} pair denoting the QuestDB server. By default, port 9000 for HTTP, port 9009 for TCP. |
| `protocol` (schema) | `http` | The transport protocol to use. Options are http(s)/tcp(s)/ws(s). `ws::` / `wss::` requires .NET 7+. |
| `addr` | `localhost:9000` | The {host}:{port} pair denoting the QuestDB server. Default port 9000 for HTTP and ws/wss, 9009 for TCP. |
| `auto_flush` | `on` | Enables or disables auto-flushing functionality. By default, the buffer will be flushed every 75,000 rows, or every 1000ms, whichever comes first. |
| `auto_flush_rows` | `75000 (HTTP)` `600 (TCP)` | The row count after which the buffer will be flushed. Effectively a batch size. |
| `auto_flush_bytes` | `Int.MaxValue` | The byte buffer length which when exceeded, will trigger a flush. |
Expand All @@ -171,6 +292,29 @@ The config string format is:
| `max_name_len` | `127` | The maximum allowed bytes, in UTF-8 format, for column and table names. |
| `protocol_version` | | Explicitly specifies the version of InfluxDB Line Protocol to use for sender. Valid options are:<br>• protocol_version=1<br>• protocol_version=2<br>• protocol_version=3<br>• protocol_version=auto (default, if unspecified) |

### WebSocket / QWP-only parameters

| Name | Default | Description |
| --------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- |
| `in_flight_window` | `128` | Max pipelined batches awaiting ACK. Set to `1` for synchronous send-and-wait semantics. |
| `close_timeout` | `5000` ms | Per-flush ACK-drain timeout, applied to `Send` and `Dispose`. |
| `max_schemas_per_connection` | `65535` | Per-connection cap on distinct schema IDs. Hitting it requires recreating the sender. |
| `gorilla` | `off` | `on` / `off` — enables Gorilla DoD compression on timestamp columns. |
| `request_durable_ack` | `off` | `on` / `off` — opts into per-table object-store ACK watermarks (cast to `IQwpWebSocketSender`). |
| `sf_dir` | | Path to a local directory enabling store-and-forward. Sets the SF stack on this sender. |
| `sender_id` | `default` | Slot identifier under `<sf_dir>/<sender_id>/`. Must be unique per process sharing the same `sf_dir`. |
| `sf_max_bytes` | `67108864` | Per-segment rotation threshold in bytes (default 64 MiB). |
| `sf_max_total_bytes` | `long.MaxValue` | Hard cap on total disk usage; back-pressures the producer when exceeded. |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
| `sf_durability` | `memory` | Durability mode. Only `memory` is supported in v1. |
| `sf_append_deadline_millis` | `30000` | Max wait when the disk cap is hit before `Send` throws. |
| `reconnect_initial_backoff_millis`| `100` | Starting backoff for reconnect attempts. |
| `reconnect_max_backoff_millis` | `30000` | Cap on per-attempt backoff. |
| `reconnect_max_duration_millis` | `300000` | Total per-outage budget; sender becomes terminal if exceeded. |
| `initial_connect_retry` | `off` | `on` makes the first connect honour the same backoff loop. Default is "fail fast on first connect". |
| `close_flush_timeout_millis` | `5000` | Max wait at `Dispose` for the SF engine to drain. `0` or `-1` for fast close. |
| `drain_orphans` | `off` | `on` adopts unlocked sibling slots on startup and drains them in the background. |
| `max_background_drainers` | `4` | Cap on concurrent orphan-drain workers. |

### Protocol Version

Behavior details:
Expand Down
Loading
Loading