Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
a8358f9
feat(qwp): add checkError, hasRowInProgress and pendingRowCount probe…
jovfer Oct 14, 2026
0f9155e
feat(pool)!: pooled close() releases the slot without flushing; flush…
jovfer Oct 14, 2026
030c53a
test(pool): opt the flush-failure discard test into flush_on_close
jovfer Oct 14, 2026
5c9f15c
test(pool): alphabetical test order, no reflection in the cancel-fail…
jovfer Oct 14, 2026
e15d20a
feat(pool): settleIdle ships rows left in idle slots through an inter…
jovfer Oct 14, 2026
52cc2b1
fix(pool): deterministic settle tests, retire on Error, log retired s…
jovfer Oct 14, 2026
ddaaa46
feat(pool): housekeeper settles idle slots on its own cadence
jovfer Oct 14, 2026
81cba03
feat(pool): flush_on_close key and builder option; settle cadence fol…
jovfer Oct 14, 2026
f99cd3c
fix(pool): parse auto_flush_interval for the settle cadence with the …
jovfer Oct 14, 2026
8a22d6a
test(pool): end-to-end close/flush/settle behaviour on the wire
jovfer Oct 14, 2026
5f5f51a
fix(pool): drop the unreachable auto_flush_interval=off branch from t…
jovfer Oct 14, 2026
0b10240
docs(pool): close releases, flush ships, drain waits; adjust tests th…
jovfer Oct 14, 2026
74343ba
feat(qwp): rowsAppended, flushDeferred and hasDeferredFrames on Sender
jovfer Oct 14, 2026
9884f80
fix(qwp): sort rowsAppended, pin no-commit-message on the wire, docum…
jovfer Oct 14, 2026
d252415
feat(pool): commit window: deferred frames per close, one commit per …
jovfer Oct 14, 2026
a3a333a
fix(pool): reject negative commit windows, pin the settle reset, tigh…
jovfer Oct 14, 2026
4a15032
feat(pool): commit_rows and commit_interval_ms keys; window requires …
jovfer Oct 14, 2026
27b7082
fix(pool): keep build() idempotent for the commit window; range-check…
jovfer Oct 14, 2026
6803675
test(pool): commit window on the wire: flags, age, settle commit, ter…
jovfer Oct 14, 2026
2ce9706
fix(pool): pooled close() wraps the delegate's error so try-with-reso…
jovfer Oct 14, 2026
e2cae49
fix(pool): keep retryable on the close() wrapper; order handler membe…
jovfer Oct 14, 2026
9ffb041
docs(pool): commit window keys; add the borrow-per-row benchmark
jovfer Oct 14, 2026
58e7102
test(pool): restore @Ignore on the benchmark
jovfer Oct 14, 2026
21bfb59
fix(pool): surface shutdown and settle ship failures, keep the window…
jovfer Oct 14, 2026
0b2d5ba
test(pool): make the window-reset tests fail when drain() or flush() …
jovfer Oct 15, 2026
b97e99c
feat(qwp): bound the byte trigger by the server's batch cap when auto…
jovfer Oct 15, 2026
3cc1507
feat(qwp): table buffer savepoint: mark a row boundary and roll back …
jovfer Oct 15, 2026
90d2654
fix(qwp): retainInProgressRow moves the table buffer savepoint like r…
jovfer Oct 15, 2026
4f8fa65
fix(qwp): savepoint rollback keeps a retained column's geohash precis…
jovfer Oct 15, 2026
557152d
feat(qwp): markSavepoint and resetToSavepoint on Sender; checkError r…
jovfer Oct 15, 2026
142dc25
fix(qwp): resetToSavepoint restores the symbol watermark and drops un…
jovfer Oct 15, 2026
e43a6d8
fix(qwp): resetToSavepoint reclaims the symbol ids the dropped rows r…
jovfer Oct 15, 2026
23f4f0b
fix(pool): reset() on a lease discards only that lease's rows
jovfer Oct 15, 2026
13838bf
fix(pool): retire a slot whose batch the server cap rejects
jovfer Oct 15, 2026
66b8739
fix(pool): place pendingRowsOf after hasUnshippedWork; say unshipped,…
jovfer Oct 15, 2026
41ab789
fix(pool): retry failed startup recovery once a second, saturate hous…
jovfer Oct 15, 2026
0915532
fix(pool): place deadline() and the recovery constant at their alphab…
jovfer Oct 15, 2026
287f29d
test(pool): pin the window age restart, flushAndGetSequence's mark, t…
jovfer Oct 15, 2026
c42d7a5
docs(pool): state frame-level fate sharing instead of promising repla…
jovfer Oct 15, 2026
f586ac5
docs(pool): remove the last example comment that says close() flushes…
jovfer Oct 15, 2026
219144b
fix(pool): a lease whose row crosses the cap margin no longer takes e…
jovfer Oct 15, 2026
0b23f31
fix(pool): pool messages drop the standalone remedy, client-side drop…
jovfer Oct 15, 2026
89ee6d9
fix(pool): mark the borrow savepoint after releasing the pool lock
jovfer Oct 16, 2026
24be1a0
fix(pool): a release ships rows older than auto_flush_interval
jovfer Oct 16, 2026
f26fa8d
feat(qwp): a table buffer can hold back its last committed row
jovfer Oct 16, 2026
7747319
fix(qwp): a row that pushes the batch over the cap ships the rows bef…
jovfer Oct 16, 2026
c9259f8
fix(qwp): a held row alone in its table keeps the savepoint columns
jovfer Oct 16, 2026
d2bba3b
fix(qwp): a held row over the byte budget flushes on its own at once
jovfer Oct 16, 2026
49bf8ae
fix(pool): housekeeper deadlines follow a monotonic clock
jovfer Oct 16, 2026
95d540f
fix(pool): settle takes one idle slot at a time
jovfer Oct 16, 2026
4255613
fix(qwp): appendBlocking parks instead of spinning when interrupted
jovfer Oct 16, 2026
8b29691
test(qwp): drop debug print from appendBlocking interrupt test
jovfer Oct 16, 2026
df6b841
fix(qwp): an empty savepoint stores symbol watermark -1
jovfer Oct 16, 2026
b5d5fb4
docs(pool): scope rejection isolation to no commit window; state the …
jovfer Oct 16, 2026
b2de8e4
fix(qwp): final review round-2 fix-ups
jovfer Oct 16, 2026
d8ab9be
fix(qwp): keep released rows visible at pool close and reap
jovfer Oct 17, 2026
47603c7
docs(pool): describe what a release ships accurately
jovfer Oct 17, 2026
5813115
test(pool): remove the ack-all borrow-per-row benchmark
jovfer Oct 21, 2026
2646250
docs(pool): drop the deleted benchmark from FrameLog's Javadoc
jovfer Oct 21, 2026
4cbbb2f
Merge remote-tracking branch 'origin/main' into sm_pool_flush_on_close
jovfer Oct 21, 2026
d903814
feat(qwp): add Sender.tryFlush(), a flush that does not wait for a fu…
jovfer Oct 21, 2026
00aa6bc
fix(pool): settle without blocking on a full ring
jovfer Oct 21, 2026
ac250fb
fix(qwp): report a failed dictionary recycle through checkError()
jovfer Oct 21, 2026
26ba55d
docs(pool): describe what a release ships, LIFO reuse and close-time …
jovfer Oct 21, 2026
1bbbb9b
docs(pool): name 1.3.11 as the release that changes the pooled close
jovfer Oct 21, 2026
c53c8eb
chore(pool): polish after the release-readiness review
jovfer Oct 21, 2026
69bbf44
Merge remote-tracking branch 'origin/main' into sm_pool_flush_on_close
jovfer Oct 21, 2026
4afe650
docs(pool): name 1.3.12 as the release that changes the pooled close
jovfer Oct 21, 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
40 changes: 32 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ try (QuestDB db = QuestDB.connect("ws::addr=localhost:9000;")) {
.doubleColumn("price", 2615.54)
.doubleColumn("amount", 0.00044)
.atNow();
// close() flushes pending rows and returns the Sender to the pool.
// close() returns the Sender to the pool; rows ship once an auto-flush threshold trips (age, rows or bytes), or when the idle slot is settled.
}
}
```
Expand Down Expand Up @@ -98,8 +98,15 @@ snippets below borrow from its pools.

### Ingest Data

Borrow a `Sender`, write rows, and close it. `close()` flushes pending rows and returns the sender to the pool — the
underlying connection is only torn down when the `QuestDB` handle itself is closed.
Borrow a `Sender`, write rows, and close it. Close releases, flush ships, drain waits: `close()` returns the sender to
the pool and ships the slot's pending rows once the oldest is older than `auto_flush_interval` (100 ms by default) or
another auto-flush threshold trips, otherwise it only releases; rows left in the slot go out when an auto-flush
threshold trips on a later append or `close()`, or when the pool's housekeeper settles the idle slot, typically within
one `auto_flush_interval`. A lease that holds the slot without appending or closing, for example one that waits for input
inside the `try` block, holds those rows back until it does: borrow once the data is in hand. Borrowing per
message is the intended pattern and batches like a sender held for the whole run. Call `flush()` to ship now and
`drain()` to wait for the server. The underlying connection is only torn down when the `QuestDB` handle itself is
closed.

```java
import io.questdb.client.Sender;
Expand All @@ -110,15 +117,29 @@ try (Sender sender = db.borrowSender()) {
.doubleColumn("price", 42_500.50)
.longColumn("size", 100)
.atNow();
// No explicit flush() needed: close() flushes for you.
// close() returns the sender; the pool ships the row by threshold or on its next settle.
}
```

### Sending Batches of Rows

The sender buffers rows and sends them in batches. For high throughput, write many rows on one borrowed sender and
flush per batch rather than per row: each flush has a fixed cost, so batching raises throughput. `close()` flushes the
final partial batch.
flush per batch rather than per row: each flush has a fixed cost, so batching raises throughput. The final partial batch ships on the
pool's next settle; call `flush()` before `close()` if you need it on the wire now.

If you borrow per message and want each message in the store-and-forward ring the moment `close()` returns, set
`flush_on_close=on` together with a commit window (`commit_rows`, `commit_interval_ms`): every close ships one frame
flagged defer-commit, the server appends it, and the pool commits once per window instead of once per close. A rejected
frame then rolls back the whole open group, so every lease since the last commit loses its rows.

A server rejection covers a whole frame. With the default `flush_on_close=off` one frame carries the rows of every
lease that closed since the last ship, so one rejected row discards the other leases' rows in that frame as well,
after their `close()` calls returned. The default error handler logs the rejection with the frame's sequence span, and
the error is thrown by the next call on that slot, possibly to a lease that wrote none of the rejected rows. A
retriable rejection (write error, internal error) replays the frame; a terminal one (schema mismatch, parse error,
security error) retires the slot, and with `sf_dir` its unacked frames stay in the slot directory for recovery. A
lease whose rows must not share a frame calls `flush()` before `close()`, or the handle sets `flush_on_close=on`.
`reset()` on a borrowed sender discards only the rows appended through that lease.

```java
try (Sender sender = db.borrowSender()) {
Expand All @@ -134,7 +155,7 @@ try (Sender sender = db.borrowSender()) {
sender.flush();
}
}
// Remaining rows are flushed by close().
// Remaining rows ship on the pool's next settle; flush() ships them now.
}
```

Expand Down Expand Up @@ -652,13 +673,16 @@ schema::key1=value1;key2=value2;
| Key | Default | Description |
| ------------------------ | ------- | ------------------------------------------------------------------------------ |
| `lazy_connect` | `off` | Tolerant startup: async ingest + lazy reads so `build()` succeeds server-down |
| `sender_pool_min` | `1` | Minimum ingest connections kept warm (`0` lets the pool drain when idle) |
| `sender_pool_min` | `1` | Minimum ingest connections kept warm (`0` lets the pool drain when idle). The free list is LIFO, so under steady load the pool shrinks to its working set and reopens connections on a burst; set it equal to `sender_pool_max` to keep every connection warm |
| `sender_pool_max` | `4` | Maximum ingest connections |
| `query_pool_min` | `1` | Minimum query connections kept warm (`0` under `lazy_connect`) |
| `query_pool_max` | `4` | Maximum query connections |
| `acquire_timeout_ms` | `5000` | How long `borrowSender()`/`borrowQuery()` waits for a free slot |
| `idle_timeout_ms` | `60000` | How long a pooled connection may stay idle before it is reaped |
| `max_lifetime_ms` | `1800000` | Maximum lifetime of a pooled connection before it is recycled |
| `flush_on_close` | `off` | `on` makes a pooled `close()` flush before releasing (one frame and one commit per close, the behaviour before 1.3.12). Note: "flush" means published to the store-and-forward ring, not acknowledged. Close-time errors are wrapped; the delegate's error is the cause; only `LineSenderServerException` keeps its type, so code that catches another `LineSenderException` subclass around a pooled `close()` must look at `getCause()`; with `on` and no commit window each lease ships its own frame, so a server rejection cannot discard other leases' rows; with `off` leases share frames and a rejection covers all of them; with a commit window a rejection rolls back the whole open group, every lease since the last commit |
| `commit_rows` | _(unset)_ | Commit window: with `flush_on_close=on`, closes ship frames flagged defer-commit and the server commits once per window of this many rows. Acks arrive at the commit; `drain()` after `flush()` |
| `commit_interval_ms` | _(unset)_ | Commit window age in milliseconds; setting either window key enables the window, the other defaults to 1000 rows / 100 ms. An idle slot's open group is committed at the settle cadence, so the interval only bounds groups on busy slots |

### Query routing keys

Expand Down
29 changes: 26 additions & 3 deletions core/src/main/java/io/questdb/client/QuestDB.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,32 @@ static QuestDB connect(CharSequence configurationString, HttpTokenProvider token
/**
* Borrows a {@link Sender} from the pool. The caller MUST call
* {@link Sender#close()} on the returned instance to release it back to
* the pool. {@code close()} on a pooled Sender flushes pending rows
* before returning to the pool; a real disconnect only happens at
* {@link #close()} on this {@code QuestDB} handle.
* the pool. Close releases, flush ships, drain waits: {@code close()} on a
* pooled Sender returns the slot and by default ships the slot's pending
* rows once the oldest is older than {@code auto_flush_interval} or another
* auto-flush threshold trips, otherwise it only releases; rows left in the
* slot go out when an auto-flush threshold trips on a later append or
* release, or when the housekeeper settles the idle slot, within about one
* {@code auto_flush_interval} of it going idle. A lease that holds the slot
* without appending or closing holds those rows back until it does. Call
* {@link Sender#flush()} to ship now and
* {@link Sender#drain(long)} to wait for the server. Set
* {@code flush_on_close=on} to have {@code close()} flush, as before 1.3.12.
* With the default, one frame carries the rows of every lease that closed
* since the last ship, so a server rejection of that frame discards those
* leases' rows together; the error is logged by the error handler and thrown
* by a later call on that slot. Call {@link Sender#flush()} before close, or
* set {@code flush_on_close=on}, when a lease's rows must not share a frame.
* {@link Sender#reset()} on a pooled Sender discards only that lease's rows.
* Note that in this client "flush" means published to the store-and-forward
* ring, not acknowledged by the server. The free list is LIFO, so under
* steady load the pool shrinks to its working set and reopens connections
* on a burst; set {@code sender_pool_min} equal to {@code sender_pool_max}
* to keep every connection warm. Close-time errors are wrapped: only
* {@code LineSenderServerException} keeps its type, so code that catches
* another {@code LineSenderException} subclass around a pooled
* {@code close()} must look at {@code getCause()}. A real disconnect only
* happens at {@link #close()} on this {@code QuestDB} handle.
* <p>
* Allocation: zero at steady state -- the returned instance is a
* pre-allocated decorator backed by a pre-allocated underlying Sender.
Expand Down
Loading
Loading