Skip to main content

ytsaurus_rpc/
blocking.rs

1//! A blocking facade, in the shape `reqwest::blocking` uses.
2//!
3//! The RPC client is `async` because multiplexed in-flight requests are the
4//! entire reason to speak this protocol. But a MapReduce job is a synchronous,
5//! single-purpose process, and enriching rows from a dynamic table mid-map is
6//! the obvious use — so there has to be a way in that does not ask the caller
7//! for a runtime.
8//!
9//! This owns a current-thread runtime and drives each call to completion on it.
10//! It is what implements [`ytsaurus_api::TableClient`], so a caller can hold one
11//! interface and choose the transport at construction, as the C++ client does.
12//!
13//! **It gives up the concurrency.** One call at a time, and the multiplexing
14//! the connection is capable of goes unused. Anything that wants it should use
15//! [`crate::Client`] directly and bring its own runtime.
16
17use std::sync::Arc;
18
19use tokio::runtime::Runtime;
20use ytsaurus_api::{
21    Error as ApiError, LookupOptions, MaybeRow, Result as ApiResult, Row, SelectOptions,
22    TableClient, TableTransaction, Transport,
23};
24
25use crate::client::{Client as AsyncClient, StartTransactionOptions, Transaction, TransactionType};
26use crate::wire;
27
28mod convert;
29
30pub use convert::{row_from_wire, row_to_wire};
31
32/// A synchronous RPC-proxy client.
33///
34/// Cheap to clone in the sense that matters — the runtime and connection are
35/// shared — but it is not `Clone`, because two handles driving one
36/// current-thread runtime from two threads would serialise on it in a way that
37/// looks like a deadlock rather than like contention.
38pub struct Client {
39    runtime: Arc<Runtime>,
40    inner: Arc<AsyncClient>,
41}
42
43impl Client {
44    /// Connects to an RPC proxy at `host:port`.
45    pub fn connect(address: &str) -> ApiResult<Self> {
46        Self::builder(address).connect()
47    }
48
49    /// Starts configuring a client.
50    pub fn builder(address: &str) -> ClientBuilder {
51        ClientBuilder {
52            address: address.to_owned(),
53            token: None,
54            timeout: None,
55        }
56    }
57}
58
59/// Configuration for [`Client::connect`].
60pub struct ClientBuilder {
61    address: String,
62    token: Option<String>,
63    timeout: Option<std::time::Duration>,
64}
65
66impl ClientBuilder {
67    /// The token sent with every request.
68    pub fn token(mut self, token: impl Into<String>) -> Self {
69        self.token = Some(token.into());
70        self
71    }
72
73    /// The per-request deadline.
74    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
75        self.timeout = Some(timeout);
76        self
77    }
78
79    pub fn connect(self) -> ApiResult<Client> {
80        // Current-thread: this facade drives one call at a time, so a
81        // multi-thread runtime would cost threads for concurrency that is not
82        // there. `enable_all` because the connection needs the timer and the
83        // network driver.
84        let runtime = tokio::runtime::Builder::new_current_thread()
85            .enable_all()
86            .build()
87            .map_err(|error| ApiError::transport_from("starting a runtime", error))?;
88
89        let mut builder = AsyncClient::builder(&self.address);
90        if let Some(token) = self.token {
91            builder = builder.token(token);
92        }
93        if let Some(timeout) = self.timeout {
94            builder = builder.timeout(timeout);
95        }
96
97        let inner = runtime
98            .block_on(builder.connect())
99            .map_err(|error| ApiError::transport_from("connecting", error))?;
100
101        Ok(Client {
102            runtime: Arc::new(runtime),
103            inner: Arc::new(inner),
104        })
105    }
106}
107
108/// Maps this crate's error onto the interface's.
109fn map_error(operation: &str, error: crate::Error) -> ApiError {
110    match &error {
111        crate::Error::Response {
112            error: reported, ..
113        } => {
114            let code = Some(reported.code);
115            ApiError::cluster_from(operation, code, error)
116        }
117        crate::Error::Timeout { .. } => ApiError::Timeout {
118            operation: operation.to_owned(),
119        },
120        _ => ApiError::transport_from(operation, error),
121    }
122}
123
124/// The columns a set of rows mentions, in first-seen order.
125///
126/// The wire format numbers values and resolves them through a name table, so
127/// every row in one request has to agree on that table. Rows that name their
128/// columns individually are folded into one here.
129fn column_names(rows: &[Row]) -> Vec<String> {
130    let mut names: Vec<String> = Vec::new();
131    for row in rows {
132        for name in row.names() {
133            if !names.iter().any(|known| known == name) {
134                names.push(name.to_owned());
135            }
136        }
137    }
138    names
139}
140
141fn to_wire_rows(rows: &[Row], columns: &[String]) -> ApiResult<Vec<wire::Row>> {
142    rows.iter().map(|row| row_to_wire(row, columns)).collect()
143}
144
145fn from_wire_rows(rows: Vec<MaybeRowWire>, columns: &[String]) -> ApiResult<Vec<MaybeRow>> {
146    rows.into_iter()
147        .map(|row| match row {
148            Some(row) => row_from_wire(&row, columns).map(Some),
149            None => Ok(None),
150        })
151        .collect()
152}
153
154type MaybeRowWire = Option<wire::Row>;
155
156/// Maps the common options onto the RPC request without changing the query
157/// text. `TReqSelectRows` has an `output_row_limit` field for exactly this.
158fn rpc_select_options(options: &SelectOptions) -> crate::client::SelectOptions {
159    crate::client::SelectOptions {
160        timestamp: options.timestamp,
161        output_row_limit: options.limit,
162    }
163}
164
165impl TableClient for Client {
166    fn transport(&self) -> Transport {
167        Transport::Rpc
168    }
169
170    fn lookup_rows(
171        &self,
172        path: &str,
173        keys: &[Row],
174        options: &LookupOptions,
175    ) -> ApiResult<Vec<MaybeRow>> {
176        let key_columns = column_names(keys);
177        let wire_keys = to_wire_rows(keys, &key_columns)?;
178        let borrowed: Vec<&str> = key_columns.iter().map(String::as_str).collect();
179        let filter: Vec<&str> = options.columns.iter().map(String::as_str).collect();
180
181        let (rows, columns) = self
182            .runtime
183            .block_on(self.inner.lookup_rows_with_columns(
184                path,
185                &borrowed,
186                &wire_keys,
187                crate::client::LookupOptions {
188                    timestamp: options.timestamp,
189                    column_filter: filter,
190                },
191            ))
192            .map_err(|error| map_error("lookup_rows", error))?;
193        from_wire_rows(rows, &columns)
194    }
195
196    fn select_rows(&self, query: &str, options: &SelectOptions) -> ApiResult<Vec<Row>> {
197        let (rows, columns) = self
198            .runtime
199            .block_on(
200                self.inner
201                    .select_rows_with_columns(query, rpc_select_options(options)),
202            )
203            .map_err(|error| map_error("select_rows", error))?;
204
205        rows.into_iter()
206            .flatten()
207            .map(|row| row_from_wire(&row, &columns))
208            .collect()
209    }
210
211    fn insert_rows(&self, path: &str, rows: &[Row]) -> ApiResult<()> {
212        // The RPC proxy has no standalone insert: writes belong to a tablet
213        // transaction. HTTP's `insert_rows` opens one implicitly, so this does
214        // the same and the two behave alike.
215        let transaction = self.start_transaction()?;
216        transaction.insert_rows(path, rows)?;
217        transaction.commit()
218    }
219
220    fn delete_rows(&self, path: &str, keys: &[Row]) -> ApiResult<()> {
221        let transaction = self.start_transaction()?;
222        transaction.delete_rows(path, keys)?;
223        transaction.commit()
224    }
225
226    fn start_transaction(&self) -> ApiResult<Box<dyn TableTransaction + '_>> {
227        let transaction = self
228            .runtime
229            .block_on(
230                self.inner
231                    .start_transaction(TransactionType::Tablet, StartTransactionOptions::default()),
232            )
233            .map_err(|error| map_error("start_transaction", error))?;
234
235        Ok(Box::new(BlockingTransaction {
236            runtime: Arc::clone(&self.runtime),
237            transaction,
238        }))
239    }
240}
241
242/// A tablet transaction, driven synchronously.
243struct BlockingTransaction<'a> {
244    runtime: Arc<Runtime>,
245    transaction: Transaction<'a>,
246}
247
248impl TableTransaction for BlockingTransaction<'_> {
249    fn id(&self) -> String {
250        self.transaction.id().to_string()
251    }
252
253    fn lookup_rows(
254        &self,
255        path: &str,
256        keys: &[Row],
257        options: &LookupOptions,
258    ) -> ApiResult<Vec<MaybeRow>> {
259        let key_columns = column_names(keys);
260        let wire_keys = to_wire_rows(keys, &key_columns)?;
261        let borrowed: Vec<&str> = key_columns.iter().map(String::as_str).collect();
262        let filter: Vec<&str> = options.columns.iter().map(String::as_str).collect();
263
264        let (rows, columns) = self
265            .runtime
266            .block_on(self.transaction.lookup_rows_with_columns(
267                path,
268                &borrowed,
269                &wire_keys,
270                crate::client::LookupOptions {
271                    timestamp: None,
272                    column_filter: filter,
273                },
274            ))
275            .map_err(|error| map_error("lookup_rows", error))?;
276        from_wire_rows(rows, &columns)
277    }
278
279    fn select_rows(&self, query: &str, options: &SelectOptions) -> ApiResult<Vec<Row>> {
280        let (rows, columns) = self
281            .runtime
282            .block_on(
283                self.transaction
284                    .select_rows_with_columns(query, rpc_select_options(options)),
285            )
286            .map_err(|error| map_error("select_rows", error))?;
287
288        rows.into_iter()
289            .flatten()
290            .map(|row| row_from_wire(&row, &columns))
291            .collect()
292    }
293
294    fn insert_rows(&self, path: &str, rows: &[Row]) -> ApiResult<()> {
295        let columns = column_names(rows);
296        let wire_rows = to_wire_rows(rows, &columns)?;
297        let borrowed: Vec<&str> = columns.iter().map(String::as_str).collect();
298        self.runtime
299            .block_on(self.transaction.insert_rows(path, &borrowed, &wire_rows))
300            .map_err(|error| map_error("insert_rows", error))
301    }
302
303    fn delete_rows(&self, path: &str, keys: &[Row]) -> ApiResult<()> {
304        let columns = column_names(keys);
305        let wire_keys = to_wire_rows(keys, &columns)?;
306        let borrowed: Vec<&str> = columns.iter().map(String::as_str).collect();
307        self.runtime
308            .block_on(self.transaction.delete_rows(path, &borrowed, &wire_keys))
309            .map_err(|error| map_error("delete_rows", error))
310    }
311
312    fn ping(&self) -> ApiResult<()> {
313        self.runtime
314            .block_on(self.transaction.ping())
315            .map_err(|error| map_error("ping_transaction", error))
316    }
317
318    fn commit(self: Box<Self>) -> ApiResult<()> {
319        let this = *self;
320        this.runtime
321            .block_on(this.transaction.commit())
322            .map_err(|error| map_error("commit_transaction", error))
323    }
324
325    fn abort(self: Box<Self>) -> ApiResult<()> {
326        let this = *self;
327        this.runtime
328            .block_on(this.transaction.abort())
329            .map_err(|error| map_error("abort_transaction", error))
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn select_limit_is_an_rpc_field_and_leaves_the_query_alone() {
339        let query = "* from [//tmp/t] limit 100;";
340        let options = SelectOptions {
341            timestamp: Some(99),
342            limit: Some(10),
343        };
344
345        let rpc = rpc_select_options(&options);
346        assert_eq!(rpc.timestamp, Some(99));
347        assert_eq!(rpc.output_row_limit, Some(10));
348        assert_eq!(query, "* from [//tmp/t] limit 100;");
349    }
350}