quack-ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/LICENSE-DuckDB +7 -0
- data/LICENSE-QuackProtocol +21 -0
- data/LICENSE-Rust-dependencies.json +2057 -0
- data/README.md +229 -0
- data/ext/quack/Cargo.lock +1803 -0
- data/ext/quack/Cargo.toml +17 -0
- data/ext/quack/extconf.rb +4 -0
- data/ext/quack/src/lib.rs +647 -0
- data/ext/quack/vendor/quack_protocol/Cargo.lock +1907 -0
- data/ext/quack/vendor/quack_protocol/Cargo.toml +41 -0
- data/ext/quack/vendor/quack_protocol/LICENSE +21 -0
- data/ext/quack/vendor/quack_protocol/PATCHES.md +16 -0
- data/ext/quack/vendor/quack_protocol/README.md +272 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/columns.rs +399 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/mod.rs +134 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/schema.rs +103 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/tests.rs +851 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/values.rs +150 -0
- data/ext/quack/vendor/quack_protocol/src/binary.rs +504 -0
- data/ext/quack/vendor/quack_protocol/src/builders.rs +217 -0
- data/ext/quack/vendor/quack_protocol/src/client.rs +1486 -0
- data/ext/quack/vendor/quack_protocol/src/constants.rs +11 -0
- data/ext/quack/vendor/quack_protocol/src/errors.rs +103 -0
- data/ext/quack/vendor/quack_protocol/src/inspection.rs +250 -0
- data/ext/quack/vendor/quack_protocol/src/json.rs +225 -0
- data/ext/quack/vendor/quack_protocol/src/lib.rs +54 -0
- data/ext/quack/vendor/quack_protocol/src/logical_types.rs +977 -0
- data/ext/quack/vendor/quack_protocol/src/messages.rs +843 -0
- data/ext/quack/vendor/quack_protocol/src/pool.rs +741 -0
- data/ext/quack/vendor/quack_protocol/src/protocol_tests.rs +759 -0
- data/ext/quack/vendor/quack_protocol/src/proxy_error.rs +77 -0
- data/ext/quack/vendor/quack_protocol/src/sql.rs +359 -0
- data/ext/quack/vendor/quack_protocol/src/tls.rs +354 -0
- data/ext/quack/vendor/quack_protocol/src/values.rs +77 -0
- data/ext/quack/vendor/quack_protocol/src/vector.rs +1364 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/key.der +0 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/spki.der +0 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/v1-cert.der +0 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/v3-cert.der +0 -0
- data/lib/quack/codec.rb +263 -0
- data/lib/quack/connection.rb +131 -0
- data/lib/quack/logical_types.rb +72 -0
- data/lib/quack/result.rb +64 -0
- data/lib/quack/sql.rb +183 -0
- data/lib/quack/types.rb +40 -0
- data/lib/quack/variant.rb +298 -0
- data/lib/quack.rb +34 -0
- metadata +136 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
//! A pool of Quack sessions, for running queries concurrently.
|
|
2
|
+
|
|
3
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
4
|
+
use std::sync::{Arc, Mutex, PoisonError};
|
|
5
|
+
|
|
6
|
+
use async_stream::try_stream;
|
|
7
|
+
use futures_util::StreamExt;
|
|
8
|
+
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
|
|
9
|
+
|
|
10
|
+
use crate::builders::ColumnDefinition;
|
|
11
|
+
use crate::client::{
|
|
12
|
+
QuackClient, QuackClientOptions, QuackConnectionInfo, QuackResultStream, QueryMetadata,
|
|
13
|
+
};
|
|
14
|
+
use crate::errors::{QuackError, Result};
|
|
15
|
+
use crate::sql::SqlParameters;
|
|
16
|
+
use crate::vector::{DataChunk, Row, Value};
|
|
17
|
+
|
|
18
|
+
/// Connections a [`QuackPool`] opens when no size is chosen.
|
|
19
|
+
pub const DEFAULT_MAX_CONNECTIONS: usize = 4;
|
|
20
|
+
|
|
21
|
+
#[derive(Clone, Debug)]
|
|
22
|
+
pub struct QuackPoolOptions {
|
|
23
|
+
/// Upper bound on sessions open against the server at once. Callers past
|
|
24
|
+
/// the limit wait for one to come free. Each one is a DuckDB connection on
|
|
25
|
+
/// the server, so a query engine should set this to the number of scans it
|
|
26
|
+
/// wants running in parallel.
|
|
27
|
+
///
|
|
28
|
+
/// It must be at least as large as the number of result streams a caller
|
|
29
|
+
/// holds open at the same time. Reading two streams in step from a pool
|
|
30
|
+
/// that can only supply one connection waits forever: the parked stream
|
|
31
|
+
/// holds the connection the other one needs.
|
|
32
|
+
pub max_connections: usize,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl Default for QuackPoolOptions {
|
|
36
|
+
fn default() -> Self {
|
|
37
|
+
Self {
|
|
38
|
+
max_connections: DEFAULT_MAX_CONNECTIONS,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// A set of Quack sessions shared by concurrent callers.
|
|
44
|
+
///
|
|
45
|
+
/// A [`QuackClient`] is one server-side session and runs one query at a time
|
|
46
|
+
/// (see its docs for why). A `QuackPool` keeps several of those sessions and
|
|
47
|
+
/// hands a free one to each caller, so N queries run on the server at once.
|
|
48
|
+
/// Cloning the pool is cheap and shares its connections, so one pool can back
|
|
49
|
+
/// every table in a catalog.
|
|
50
|
+
///
|
|
51
|
+
/// The trade is session state. Temporary tables, `SET`, transactions, and
|
|
52
|
+
/// attached databases live on a single connection, and the pool gives no
|
|
53
|
+
/// affinity between calls - two [`query`](Self::query) calls may land on
|
|
54
|
+
/// different sessions. Work that depends on session state holds one connection
|
|
55
|
+
/// for as long as it needs it:
|
|
56
|
+
///
|
|
57
|
+
/// ```no_run
|
|
58
|
+
/// # use quack_protocol::{QuackPool, Result};
|
|
59
|
+
/// # async fn example(pool: &QuackPool) -> Result<()> {
|
|
60
|
+
/// let connection = pool.acquire().await?;
|
|
61
|
+
/// connection
|
|
62
|
+
/// .execute("CREATE TEMP TABLE staging AS SELECT 1 AS id", None)
|
|
63
|
+
/// .await?;
|
|
64
|
+
/// let ids = connection.values("SELECT id FROM staging").await?;
|
|
65
|
+
/// # let _ = ids;
|
|
66
|
+
/// # Ok(())
|
|
67
|
+
/// # }
|
|
68
|
+
/// ```
|
|
69
|
+
///
|
|
70
|
+
/// Connections open on demand up to
|
|
71
|
+
/// [`max_connections`](QuackPoolOptions::max_connections) and are reused
|
|
72
|
+
/// afterwards. A connection that saw a wire failure is retired rather than
|
|
73
|
+
/// handed on, and retiring it closes its session on the server. Nothing is
|
|
74
|
+
/// opened in its place until a later [`acquire`](Self::acquire) finds no free
|
|
75
|
+
/// connection, and the retired session keeps its slot until its DISCONNECT
|
|
76
|
+
/// has been answered, so the pool never has more sessions open than its limit.
|
|
77
|
+
#[derive(Clone, Debug)]
|
|
78
|
+
pub struct QuackPool {
|
|
79
|
+
inner: Arc<PoolInner>,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
impl QuackPool {
|
|
83
|
+
/// Connect to a Quack server and open the pool.
|
|
84
|
+
///
|
|
85
|
+
/// One connection is made now, so a bad URI, token, or protocol version
|
|
86
|
+
/// fails here rather than on the first query; the rest are opened as
|
|
87
|
+
/// concurrent callers need them.
|
|
88
|
+
pub async fn connect(
|
|
89
|
+
uri: &str,
|
|
90
|
+
options: QuackClientOptions,
|
|
91
|
+
pool_options: QuackPoolOptions,
|
|
92
|
+
) -> Result<Self> {
|
|
93
|
+
if pool_options.max_connections == 0 {
|
|
94
|
+
return Err(QuackError::protocol(
|
|
95
|
+
"QuackPoolOptions::max_connections must be at least 1",
|
|
96
|
+
));
|
|
97
|
+
}
|
|
98
|
+
let client = QuackClient::connect(uri, options.clone()).await?;
|
|
99
|
+
let info = client.info.clone();
|
|
100
|
+
|
|
101
|
+
Ok(Self {
|
|
102
|
+
inner: Arc::new(PoolInner {
|
|
103
|
+
uri: uri.to_string(),
|
|
104
|
+
options,
|
|
105
|
+
info,
|
|
106
|
+
permits: Arc::new(Semaphore::new(pool_options.max_connections)),
|
|
107
|
+
max_connections: pool_options.max_connections,
|
|
108
|
+
available: Mutex::new(vec![client]),
|
|
109
|
+
closed: AtomicBool::new(false),
|
|
110
|
+
}),
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// What the server reported when the pool's first connection was made.
|
|
115
|
+
pub fn info(&self) -> Option<&QuackConnectionInfo> {
|
|
116
|
+
self.inner.info.as_ref()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pub fn max_connections(&self) -> usize {
|
|
120
|
+
self.inner.max_connections
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Take a connection out of the pool for exclusive use.
|
|
124
|
+
///
|
|
125
|
+
/// Waits if every connection is busy. The connection returns to the pool
|
|
126
|
+
/// when the returned lease is dropped, so hold the lease for exactly as
|
|
127
|
+
/// long as the work that needs one session - and no longer.
|
|
128
|
+
pub async fn acquire(&self) -> Result<PooledClient> {
|
|
129
|
+
let permit = self.inner.permit().await?;
|
|
130
|
+
let client = match self.inner.take_available() {
|
|
131
|
+
Some(client) => client,
|
|
132
|
+
None => self.inner.connect_one().await?,
|
|
133
|
+
};
|
|
134
|
+
Ok(self.inner.lease(client, permit))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Run a query on any free connection.
|
|
138
|
+
///
|
|
139
|
+
/// The returned stream holds its connection until it is drained or
|
|
140
|
+
/// dropped, the same way [`QuackClient::query`] does - so drop it once the
|
|
141
|
+
/// results are no longer wanted, rather than leaving it parked.
|
|
142
|
+
pub async fn query(
|
|
143
|
+
&self,
|
|
144
|
+
sql: &str,
|
|
145
|
+
metadata: Option<&QueryMetadata>,
|
|
146
|
+
) -> Result<QuackResultStream> {
|
|
147
|
+
self.acquire().await?.query(sql, metadata).await
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
pub async fn query_with_params(
|
|
151
|
+
&self,
|
|
152
|
+
sql: &str,
|
|
153
|
+
params: Option<&SqlParameters>,
|
|
154
|
+
) -> Result<QuackResultStream> {
|
|
155
|
+
self.acquire().await?.query_with_params(sql, params).await
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Run a statement on any free connection and discard its result.
|
|
159
|
+
pub async fn execute(&self, sql: &str, metadata: Option<&QueryMetadata>) -> Result<()> {
|
|
160
|
+
self.query(sql, metadata).await?.drain().await
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pub async fn first(&self, sql: &str) -> Result<Option<Row>> {
|
|
164
|
+
self.query(sql, None).await?.first_row().await
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
pub async fn one(&self, sql: &str) -> Result<Row> {
|
|
168
|
+
self.query(sql, None).await?.one_row().await
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
pub async fn values(&self, sql: &str) -> Result<Vec<Value>> {
|
|
172
|
+
self.query(sql, None).await?.first_column().await
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// Append a chunk on any free connection.
|
|
176
|
+
///
|
|
177
|
+
/// Unlike a query, a failed append is never retried: a request that failed
|
|
178
|
+
/// after the server ran it is indistinguishable from one it never saw, and
|
|
179
|
+
/// appending twice is worse than failing once.
|
|
180
|
+
pub async fn append(
|
|
181
|
+
&self,
|
|
182
|
+
table_name: impl Into<String>,
|
|
183
|
+
schema_name: Option<String>,
|
|
184
|
+
chunk: DataChunk,
|
|
185
|
+
) -> Result<()> {
|
|
186
|
+
self.acquire()
|
|
187
|
+
.await?
|
|
188
|
+
.append(table_name, schema_name, chunk)
|
|
189
|
+
.await
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Append rows on one connection, in batches. Not retried, as
|
|
193
|
+
/// [`append`](Self::append) is not.
|
|
194
|
+
pub async fn append_rows(
|
|
195
|
+
&self,
|
|
196
|
+
table_name: impl Into<String>,
|
|
197
|
+
schema_name: Option<String>,
|
|
198
|
+
rows: &[Row],
|
|
199
|
+
columns: Option<Vec<ColumnDefinition>>,
|
|
200
|
+
batch_size: Option<usize>,
|
|
201
|
+
) -> Result<()> {
|
|
202
|
+
self.acquire()
|
|
203
|
+
.await?
|
|
204
|
+
.append_rows(table_name, schema_name, rows, columns, batch_size)
|
|
205
|
+
.await
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// Close every session the pool holds and refuse further connections.
|
|
209
|
+
///
|
|
210
|
+
/// Connections still leased are closed as their leases drop. Returns the
|
|
211
|
+
/// first failure, after trying to close all of them.
|
|
212
|
+
pub async fn close(&self) -> Result<()> {
|
|
213
|
+
let available = self.inner.close_and_drain();
|
|
214
|
+
// Wakes anyone waiting on `acquire` with a "pool is closed" error.
|
|
215
|
+
self.inner.permits.close();
|
|
216
|
+
|
|
217
|
+
let mut first_error = None;
|
|
218
|
+
for client in available {
|
|
219
|
+
if let Err(err) = client.disconnect().await {
|
|
220
|
+
first_error.get_or_insert(err);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
match first_error {
|
|
224
|
+
Some(err) => Err(err),
|
|
225
|
+
None => Ok(()),
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// A connection borrowed from a [`QuackPool`].
|
|
231
|
+
///
|
|
232
|
+
/// This handle may be cloned, and a query stream may outlive the handle that
|
|
233
|
+
/// created it. All such handles share ownership of the lease; the connection
|
|
234
|
+
/// returns to the pool only after the last handle or result stream is dropped.
|
|
235
|
+
///
|
|
236
|
+
/// The underlying [`QuackClient`] is intentionally not exposed. Giving out a
|
|
237
|
+
/// raw client clone would let it keep using the session after the pool had
|
|
238
|
+
/// assigned that session to another borrower.
|
|
239
|
+
///
|
|
240
|
+
/// ```compile_fail
|
|
241
|
+
/// # use quack_protocol::{QuackClient, QuackPool, Result};
|
|
242
|
+
/// # async fn cannot_extract_client(pool: &QuackPool) -> Result<()> {
|
|
243
|
+
/// let lease = pool.acquire().await?;
|
|
244
|
+
/// let raw_client = QuackClient::clone(&lease);
|
|
245
|
+
/// # let _ = raw_client;
|
|
246
|
+
/// # Ok(())
|
|
247
|
+
/// # }
|
|
248
|
+
/// ```
|
|
249
|
+
#[derive(Clone, Debug)]
|
|
250
|
+
pub struct PooledClient {
|
|
251
|
+
inner: Arc<PooledClientInner>,
|
|
252
|
+
/// Connection metadata reported when this session was opened.
|
|
253
|
+
pub info: Option<QuackConnectionInfo>,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[derive(Debug)]
|
|
257
|
+
struct PooledClientInner {
|
|
258
|
+
// Both `Some` until `Drop` hands them back.
|
|
259
|
+
client: Option<QuackClient>,
|
|
260
|
+
permit: Option<OwnedSemaphorePermit>,
|
|
261
|
+
pool: Arc<PoolInner>,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
impl PooledClient {
|
|
265
|
+
fn client(&self) -> &QuackClient {
|
|
266
|
+
self.inner
|
|
267
|
+
.client
|
|
268
|
+
.as_ref()
|
|
269
|
+
.expect("pooled client is taken only by Drop")
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
pub fn is_connected(&self) -> bool {
|
|
273
|
+
self.client().is_connected()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Run a statement on this leased session and discard its result.
|
|
277
|
+
pub async fn execute(&self, sql: &str, metadata: Option<&QueryMetadata>) -> Result<()> {
|
|
278
|
+
self.query(sql, metadata).await?.drain().await
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// Run a query on this leased session.
|
|
282
|
+
///
|
|
283
|
+
/// The returned stream retains a clone of the lease, so dropping this
|
|
284
|
+
/// handle does not return the session while the stream is still alive.
|
|
285
|
+
pub async fn query(
|
|
286
|
+
&self,
|
|
287
|
+
sql: &str,
|
|
288
|
+
metadata: Option<&QueryMetadata>,
|
|
289
|
+
) -> Result<QuackResultStream> {
|
|
290
|
+
let stream = self.client().query_inner(sql, None, metadata).await?;
|
|
291
|
+
Ok(attach_lease(stream, self.clone()))
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
pub async fn query_with_params(
|
|
295
|
+
&self,
|
|
296
|
+
sql: &str,
|
|
297
|
+
params: Option<&SqlParameters>,
|
|
298
|
+
) -> Result<QuackResultStream> {
|
|
299
|
+
let stream = self.client().query_inner(sql, params, None).await?;
|
|
300
|
+
Ok(attach_lease(stream, self.clone()))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
pub async fn first(&self, sql: &str) -> Result<Option<Row>> {
|
|
304
|
+
self.query(sql, None).await?.first_row().await
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
pub async fn one(&self, sql: &str) -> Result<Row> {
|
|
308
|
+
self.query(sql, None).await?.one_row().await
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
pub async fn values(&self, sql: &str) -> Result<Vec<Value>> {
|
|
312
|
+
self.query(sql, None).await?.first_column().await
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
pub async fn append(
|
|
316
|
+
&self,
|
|
317
|
+
table_name: impl Into<String>,
|
|
318
|
+
schema_name: Option<String>,
|
|
319
|
+
chunk: DataChunk,
|
|
320
|
+
) -> Result<()> {
|
|
321
|
+
self.client().append(table_name, schema_name, chunk).await
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
pub async fn append_rows(
|
|
325
|
+
&self,
|
|
326
|
+
table_name: impl Into<String>,
|
|
327
|
+
schema_name: Option<String>,
|
|
328
|
+
rows: &[Row],
|
|
329
|
+
columns: Option<Vec<ColumnDefinition>>,
|
|
330
|
+
batch_size: Option<usize>,
|
|
331
|
+
) -> Result<()> {
|
|
332
|
+
self.client()
|
|
333
|
+
.append_rows(table_name, schema_name, rows, columns, batch_size)
|
|
334
|
+
.await
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/// Close this leased session. The pool retires it after all lease handles
|
|
338
|
+
/// and streams have been dropped.
|
|
339
|
+
pub async fn disconnect(&self) -> Result<()> {
|
|
340
|
+
self.client().disconnect().await
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
pub async fn close(&self) -> Result<()> {
|
|
344
|
+
self.disconnect().await
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
impl Drop for PooledClientInner {
|
|
349
|
+
fn drop(&mut self) {
|
|
350
|
+
let Some(client) = self.client.take() else {
|
|
351
|
+
return;
|
|
352
|
+
};
|
|
353
|
+
let Some(retired) = self.pool.release(client) else {
|
|
354
|
+
// The permit drops next, after the connection is back in the
|
|
355
|
+
// pool, so a waiter that wakes on it finds the connection waiting.
|
|
356
|
+
return;
|
|
357
|
+
};
|
|
358
|
+
// Retired: the permit stays with the session until its DISCONNECT is
|
|
359
|
+
// answered, so the caller that wakes on it opens a replacement only
|
|
360
|
+
// once the old session is gone. Without a runtime the client's own
|
|
361
|
+
// `Drop` logs and gives up, and the permit is released here.
|
|
362
|
+
let Ok(runtime) = tokio::runtime::Handle::try_current() else {
|
|
363
|
+
return;
|
|
364
|
+
};
|
|
365
|
+
let permit = self.permit.take();
|
|
366
|
+
runtime.spawn(async move {
|
|
367
|
+
// Failures are logged by `Connection`; there is no caller to tell.
|
|
368
|
+
let _ = retired.disconnect().await;
|
|
369
|
+
drop(retired);
|
|
370
|
+
drop(permit);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#[derive(Debug)]
|
|
376
|
+
struct PoolInner {
|
|
377
|
+
uri: String,
|
|
378
|
+
options: QuackClientOptions,
|
|
379
|
+
info: Option<QuackConnectionInfo>,
|
|
380
|
+
permits: Arc<Semaphore>,
|
|
381
|
+
max_connections: usize,
|
|
382
|
+
available: Mutex<Vec<QuackClient>>,
|
|
383
|
+
closed: AtomicBool,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
impl PoolInner {
|
|
387
|
+
async fn permit(&self) -> Result<OwnedSemaphorePermit> {
|
|
388
|
+
Arc::clone(&self.permits)
|
|
389
|
+
.acquire_owned()
|
|
390
|
+
.await
|
|
391
|
+
.map_err(|_| QuackError::protocol("Quack pool is closed"))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
fn lease(self: &Arc<Self>, client: QuackClient, permit: OwnedSemaphorePermit) -> PooledClient {
|
|
395
|
+
let info = client.info.clone();
|
|
396
|
+
PooledClient {
|
|
397
|
+
inner: Arc::new(PooledClientInner {
|
|
398
|
+
client: Some(client),
|
|
399
|
+
permit: Some(permit),
|
|
400
|
+
pool: Arc::clone(self),
|
|
401
|
+
}),
|
|
402
|
+
info,
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async fn connect_one(&self) -> Result<QuackClient> {
|
|
407
|
+
if self.closed.load(Ordering::Relaxed) {
|
|
408
|
+
return Err(QuackError::protocol("Quack pool is closed"));
|
|
409
|
+
}
|
|
410
|
+
let client = QuackClient::connect(&self.uri, self.options.clone()).await?;
|
|
411
|
+
if self.closed_while_connecting() {
|
|
412
|
+
// `close()` ran while this connect was in flight, so it cannot
|
|
413
|
+
// have closed this session. Close it here rather than hand a live
|
|
414
|
+
// session out of a closed pool.
|
|
415
|
+
let _ = client.disconnect().await;
|
|
416
|
+
return Err(QuackError::protocol("Quack pool is closed"));
|
|
417
|
+
}
|
|
418
|
+
Ok(client)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// `close_and_drain` sets the flag under the available lock, so reading it
|
|
422
|
+
// there orders this check after a close that has already drained.
|
|
423
|
+
fn closed_while_connecting(&self) -> bool {
|
|
424
|
+
let _available = self.lock_available();
|
|
425
|
+
self.closed.load(Ordering::Relaxed)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
fn take_available(&self) -> Option<QuackClient> {
|
|
429
|
+
let mut available = self.lock_available();
|
|
430
|
+
// Connections retired while idle are dropped here, which closes their
|
|
431
|
+
// sessions on the server.
|
|
432
|
+
while let Some(client) = available.pop() {
|
|
433
|
+
if client.is_reusable() {
|
|
434
|
+
return Some(client);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
None
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Puts a connection back, or returns it when it must be retired instead:
|
|
441
|
+
// the pool is closed or the session saw a wire failure. The closed check
|
|
442
|
+
// happens under the available lock so that `close_and_drain` cannot run
|
|
443
|
+
// between the check and the push, leaving a live session behind in a
|
|
444
|
+
// closed pool.
|
|
445
|
+
fn release(&self, client: QuackClient) -> Option<QuackClient> {
|
|
446
|
+
let mut available = self.lock_available();
|
|
447
|
+
if self.closed.load(Ordering::Relaxed) || !client.is_reusable() {
|
|
448
|
+
return Some(client);
|
|
449
|
+
}
|
|
450
|
+
available.push(client);
|
|
451
|
+
None
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
fn close_and_drain(&self) -> Vec<QuackClient> {
|
|
455
|
+
let mut available = self.lock_available();
|
|
456
|
+
self.closed.store(true, Ordering::Relaxed);
|
|
457
|
+
std::mem::take(&mut *available)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// The lock guards a `Vec` and nothing else, so a panic elsewhere in the
|
|
461
|
+
// process must not take the pool down with it.
|
|
462
|
+
fn lock_available(&self) -> std::sync::MutexGuard<'_, Vec<QuackClient>> {
|
|
463
|
+
self.available
|
|
464
|
+
.lock()
|
|
465
|
+
.unwrap_or_else(PoisonError::into_inner)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Keeps the connection out of the pool until the caller is done with the
|
|
470
|
+
// results, the way `QuackClient` holds its own connection lock.
|
|
471
|
+
fn attach_lease(stream: QuackResultStream, lease: PooledClient) -> QuackResultStream {
|
|
472
|
+
let (columns, chunks) = stream.into_chunks();
|
|
473
|
+
let chunks = try_stream! {
|
|
474
|
+
let _lease = lease;
|
|
475
|
+
for await chunk in chunks {
|
|
476
|
+
yield chunk?;
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
QuackResultStream::new(columns, chunks.boxed())
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
#[cfg(test)]
|
|
483
|
+
mod tests {
|
|
484
|
+
use std::io::{Read, Write};
|
|
485
|
+
use std::net::TcpListener;
|
|
486
|
+
use std::thread;
|
|
487
|
+
use std::time::Duration;
|
|
488
|
+
|
|
489
|
+
use super::*;
|
|
490
|
+
use crate::binary::HugeIntParts;
|
|
491
|
+
use crate::constants::QUACK_V1;
|
|
492
|
+
use crate::messages::{MessageHeader, MessageType, QuackMessage, encode_message_for_version};
|
|
493
|
+
|
|
494
|
+
fn scripted_server(responses: Vec<QuackMessage>) -> (String, thread::JoinHandle<()>) {
|
|
495
|
+
let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server");
|
|
496
|
+
let address = listener.local_addr().expect("test server address");
|
|
497
|
+
let responses = responses
|
|
498
|
+
.into_iter()
|
|
499
|
+
.map(|response| {
|
|
500
|
+
encode_message_for_version(&response, QUACK_V1).expect("encode test response")
|
|
501
|
+
})
|
|
502
|
+
.collect::<Vec<_>>();
|
|
503
|
+
let server = thread::spawn(move || {
|
|
504
|
+
for response in responses {
|
|
505
|
+
let (mut socket, _) = listener.accept().expect("accept test request");
|
|
506
|
+
let mut request = [0; 4096];
|
|
507
|
+
let bytes_read = socket.read(&mut request).expect("read test request");
|
|
508
|
+
assert!(bytes_read > 0, "test request must not be empty");
|
|
509
|
+
write!(
|
|
510
|
+
socket,
|
|
511
|
+
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
|
512
|
+
response.len()
|
|
513
|
+
)
|
|
514
|
+
.expect("write test response headers");
|
|
515
|
+
socket.write_all(&response).expect("write test response");
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
(format!("http://{address}"), server)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
fn v1_options() -> QuackClientOptions {
|
|
522
|
+
QuackClientOptions {
|
|
523
|
+
min_supported_quack_version: Some(QUACK_V1),
|
|
524
|
+
max_supported_quack_version: Some(QUACK_V1),
|
|
525
|
+
timeout: Some(Duration::from_secs(2)),
|
|
526
|
+
..QuackClientOptions::default()
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
#[tokio::test]
|
|
531
|
+
async fn rejects_an_empty_pool() {
|
|
532
|
+
let err = QuackPool::connect(
|
|
533
|
+
"localhost:9494",
|
|
534
|
+
QuackClientOptions::default(),
|
|
535
|
+
QuackPoolOptions { max_connections: 0 },
|
|
536
|
+
)
|
|
537
|
+
.await
|
|
538
|
+
.expect_err("max_connections 0 is rejected");
|
|
539
|
+
assert!(err.to_string().contains("max_connections"), "{err}");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#[test]
|
|
543
|
+
fn default_pool_is_small() {
|
|
544
|
+
assert_eq!(
|
|
545
|
+
QuackPoolOptions::default().max_connections,
|
|
546
|
+
DEFAULT_MAX_CONNECTIONS
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[test]
|
|
551
|
+
fn cloning_a_pooled_client_produces_another_lease() {
|
|
552
|
+
let clone_lease: fn(&PooledClient) -> PooledClient = PooledClient::clone;
|
|
553
|
+
let _ = clone_lease;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
#[tokio::test]
|
|
557
|
+
async fn clones_and_query_streams_keep_the_pool_lease() {
|
|
558
|
+
let connection_response = QuackMessage::ConnectionResponse {
|
|
559
|
+
header: MessageHeader::new(MessageType::ConnectionResponse)
|
|
560
|
+
.with_connection("leased-session"),
|
|
561
|
+
server_duckdb_version: None,
|
|
562
|
+
server_platform: None,
|
|
563
|
+
quack_version: Some(QUACK_V1),
|
|
564
|
+
heartbeat_timeout_seconds: None,
|
|
565
|
+
};
|
|
566
|
+
let prepare_response = QuackMessage::PrepareResponse {
|
|
567
|
+
header: MessageHeader::new(MessageType::PrepareResponse)
|
|
568
|
+
.with_connection("leased-session"),
|
|
569
|
+
result_types: Vec::new(),
|
|
570
|
+
result_names: Vec::new(),
|
|
571
|
+
needs_more_fetch: false,
|
|
572
|
+
results: Vec::new(),
|
|
573
|
+
result_uuid: HugeIntParts { upper: 0, lower: 1 },
|
|
574
|
+
};
|
|
575
|
+
let (uri, server) = scripted_server(vec![connection_response, prepare_response]);
|
|
576
|
+
let pool = QuackPool::connect(&uri, v1_options(), QuackPoolOptions { max_connections: 1 })
|
|
577
|
+
.await
|
|
578
|
+
.expect("connect pool");
|
|
579
|
+
|
|
580
|
+
let lease = pool.acquire().await.expect("acquire lease");
|
|
581
|
+
let cloned_lease = lease.clone();
|
|
582
|
+
drop(lease);
|
|
583
|
+
assert!(
|
|
584
|
+
tokio::time::timeout(Duration::from_millis(50), pool.acquire())
|
|
585
|
+
.await
|
|
586
|
+
.is_err(),
|
|
587
|
+
"a cloned handle must keep the permit"
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
let stream = cloned_lease
|
|
591
|
+
.query("SELECT 1", None)
|
|
592
|
+
.await
|
|
593
|
+
.expect("create result stream");
|
|
594
|
+
drop(cloned_lease);
|
|
595
|
+
assert!(
|
|
596
|
+
tokio::time::timeout(Duration::from_millis(50), pool.acquire())
|
|
597
|
+
.await
|
|
598
|
+
.is_err(),
|
|
599
|
+
"a result stream must keep the permit"
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
drop(stream);
|
|
603
|
+
let returned = tokio::time::timeout(Duration::from_secs(1), pool.acquire())
|
|
604
|
+
.await
|
|
605
|
+
.expect("released stream should unblock acquire")
|
|
606
|
+
.expect("reacquire returned session");
|
|
607
|
+
drop(returned);
|
|
608
|
+
server.join().expect("test server");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
fn connection_response(connection_id: &str) -> QuackMessage {
|
|
612
|
+
QuackMessage::ConnectionResponse {
|
|
613
|
+
header: MessageHeader::new(MessageType::ConnectionResponse)
|
|
614
|
+
.with_connection(connection_id),
|
|
615
|
+
server_duckdb_version: None,
|
|
616
|
+
server_platform: None,
|
|
617
|
+
quack_version: Some(QUACK_V1),
|
|
618
|
+
heartbeat_timeout_seconds: None,
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
fn success_response() -> QuackMessage {
|
|
623
|
+
QuackMessage::SuccessResponse {
|
|
624
|
+
header: MessageHeader::new(MessageType::SuccessResponse),
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
#[tokio::test]
|
|
629
|
+
async fn a_malformed_response_retires_the_session() {
|
|
630
|
+
// SUCCESS in answer to PREPARE decodes fine but is the wrong message.
|
|
631
|
+
let (uri, server) = scripted_server(vec![
|
|
632
|
+
connection_response("malformed-session"),
|
|
633
|
+
success_response(),
|
|
634
|
+
success_response(),
|
|
635
|
+
]);
|
|
636
|
+
let client = QuackClient::connect(&uri, v1_options())
|
|
637
|
+
.await
|
|
638
|
+
.expect("connect client");
|
|
639
|
+
assert!(client.is_reusable());
|
|
640
|
+
|
|
641
|
+
let error = match client.query("SELECT 1", None).await {
|
|
642
|
+
Ok(_) => panic!("wrong response type is a protocol error"),
|
|
643
|
+
Err(error) => error,
|
|
644
|
+
};
|
|
645
|
+
assert!(
|
|
646
|
+
matches!(error, QuackError::Protocol(ref message) if message.contains("PREPARE_RESPONSE")),
|
|
647
|
+
"{error}"
|
|
648
|
+
);
|
|
649
|
+
assert!(
|
|
650
|
+
!client.is_reusable(),
|
|
651
|
+
"a session that answered out of protocol must not be handed on"
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
client.disconnect().await.expect("disconnect");
|
|
655
|
+
server.join().expect("test server");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
#[tokio::test]
|
|
659
|
+
async fn a_retired_lease_keeps_its_slot_until_the_session_is_closed() {
|
|
660
|
+
// Response order is the whole assertion: with one slot, the
|
|
661
|
+
// replacement CONNECT must come after the retired session's
|
|
662
|
+
// DISCONNECT. If the permit were released early, the replacement
|
|
663
|
+
// would consume the DISCONNECT acknowledgement and fail to connect.
|
|
664
|
+
let (uri, server) = scripted_server(vec![
|
|
665
|
+
connection_response("retired-session"),
|
|
666
|
+
success_response(), // wrong answer to PREPARE: retires the session
|
|
667
|
+
success_response(), // acknowledges DISCONNECT
|
|
668
|
+
connection_response("replacement-session"),
|
|
669
|
+
]);
|
|
670
|
+
let pool = QuackPool::connect(&uri, v1_options(), QuackPoolOptions { max_connections: 1 })
|
|
671
|
+
.await
|
|
672
|
+
.expect("connect pool");
|
|
673
|
+
|
|
674
|
+
let lease = pool.acquire().await.expect("acquire lease");
|
|
675
|
+
assert!(
|
|
676
|
+
lease.query("SELECT 1", None).await.is_err(),
|
|
677
|
+
"wrong response type is a protocol error"
|
|
678
|
+
);
|
|
679
|
+
drop(lease);
|
|
680
|
+
|
|
681
|
+
let replacement = tokio::time::timeout(Duration::from_secs(2), pool.acquire())
|
|
682
|
+
.await
|
|
683
|
+
.expect("slot frees once the retired session is closed")
|
|
684
|
+
.expect("replacement connects after the DISCONNECT");
|
|
685
|
+
assert!(replacement.is_connected());
|
|
686
|
+
drop(replacement);
|
|
687
|
+
server.join().expect("test server");
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
#[tokio::test]
|
|
691
|
+
async fn ambiguous_connection_error_text_is_returned_without_replay() {
|
|
692
|
+
let connection_response = QuackMessage::ConnectionResponse {
|
|
693
|
+
header: MessageHeader::new(MessageType::ConnectionResponse)
|
|
694
|
+
.with_connection("possibly-stale-session"),
|
|
695
|
+
server_duckdb_version: None,
|
|
696
|
+
server_platform: None,
|
|
697
|
+
quack_version: Some(QUACK_V1),
|
|
698
|
+
heartbeat_timeout_seconds: None,
|
|
699
|
+
};
|
|
700
|
+
let ambiguous_error = QuackMessage::ErrorResponse {
|
|
701
|
+
header: MessageHeader::new(MessageType::ErrorResponse),
|
|
702
|
+
message: "Invalid connection id".to_string(),
|
|
703
|
+
};
|
|
704
|
+
let disconnect_response = QuackMessage::SuccessResponse {
|
|
705
|
+
header: MessageHeader::new(MessageType::SuccessResponse),
|
|
706
|
+
};
|
|
707
|
+
let (uri, server) = scripted_server(vec![
|
|
708
|
+
connection_response,
|
|
709
|
+
ambiguous_error,
|
|
710
|
+
disconnect_response,
|
|
711
|
+
]);
|
|
712
|
+
let pool = QuackPool::connect(&uri, v1_options(), QuackPoolOptions { max_connections: 1 })
|
|
713
|
+
.await
|
|
714
|
+
.expect("connect pool");
|
|
715
|
+
|
|
716
|
+
let error = match pool.query("INSERT INTO items VALUES (1)", None).await {
|
|
717
|
+
Ok(_) => panic!("ambiguous server text must be surfaced"),
|
|
718
|
+
Err(error) => error,
|
|
719
|
+
};
|
|
720
|
+
assert!(
|
|
721
|
+
matches!(error, QuackError::Server(ref message) if message == "Invalid connection id"),
|
|
722
|
+
"the original server error must be returned: {error}"
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
// The retired session is closed on a background task, and its permit
|
|
726
|
+
// comes back only once that has finished - so waiting for the permit
|
|
727
|
+
// waits for the DISCONNECT.
|
|
728
|
+
let _permit = tokio::time::timeout(
|
|
729
|
+
Duration::from_secs(5),
|
|
730
|
+
Arc::clone(&pool.inner.permits).acquire_owned(),
|
|
731
|
+
)
|
|
732
|
+
.await
|
|
733
|
+
.expect("the retired session is closed in the background")
|
|
734
|
+
.expect("the pool is still open");
|
|
735
|
+
|
|
736
|
+
// The third response acknowledges that DISCONNECT. If the pool replayed
|
|
737
|
+
// the statement, it would consume that response as another CONNECT and
|
|
738
|
+
// return a protocol error instead of the original server error above.
|
|
739
|
+
server.join().expect("test server");
|
|
740
|
+
}
|
|
741
|
+
}
|