Skip to content

Commit 32aa75a

Browse files
authored
fix: let read-only users query Recall on ClickHouse and read each table once (#1218)
* fix: read the Recall invocations view in one pass The ClickHouse invocations view read recall_events three times and joined four subqueries, so every lookup paid for three event reads and hash-join preallocation. It is now one UNION ALL of the context rows, heartbeats and lifecycle events folded by invocation, without joins. Rows and column types are unchanged; the Recall schema version rises to 3 so writers replace the view through the existing upgrade path. Refs: CHI-773 * fix: read recall_events once in the Recall statements view The statements view read the events twice, once for statement facts and once for resource names. It now folds one read by resource, which names the resource and gathers its statement events, then by statement. sql_available stays a lookup of the stored SQL, which is shared across invocations. Refs: CHI-773 * fix: let readonly = 1 users read Recall history on ClickHouse A read connection sent SQLBuild's session settings and every recall query set readonly = 2, which a readonly = 1 user may not do. A read connection now applies the pinned session settings only when the session may change settings; a query sets readonly = 2 only when the session is not already read-only, sends the row limit only when the user may set it and otherwise stops reading at the cap, and a setting the user may not change fails with an error naming it and readonly = 2. output_invocations is folded without joins and artifacts makes its stored side nullable before the join, so no view depends on join_use_nulls or sets SETTINGS. Refs: CHI-772 * fix: read Recall tables without FINAL where folds ignore copies Copies of a Recall key hold the same content, since a write with other content is refused as a conflict. The invocations and statements views only take the first, last, smallest or largest value of a key's copies, so they read without FINAL, as do the delivery and context branches of output_invocations. Views that list or count rows keep FINAL. Refs: CHI-777 * fix: batch ClickHouse Recall writes over five seconds A long command wrote about one small part per table every second. ClickHouse batches now wait up to five seconds; heartbeats still go out at once and a closing command sends everything it holds. A batch is also sent early once a quarter of the queue's byte capacity is ready, so a byte-heavy command fills the queue no sooner than before. Refs: CHI-777 * docs: describe when a Recall reader skips session settings
1 parent 60091c6 commit 32aa75a

26 files changed

Lines changed: 1665 additions & 245 deletions

File tree

‎crates/sqlbuild-python/src/bindings/_helpers/recall/clickhouse_calls.rs‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Calls into the ClickHouse adapter's Recall store operations, through the native host.
22
33
use pyo3::prelude::{Bound, FromPyObject, PyAny, PyAnyMethods, PyErr, PyResult, Python};
4-
use pyo3::types::{PyBytes, PyDict, PyDictMethods, PyInt, PyList, PyString};
4+
use pyo3::types::{PyBool, PyBytes, PyDict, PyDictMethods, PyInt, PyList, PyString};
55
use sqlbuild_config::models::ConfigValue;
66
use sqlbuild_host::entries::models::AdapterHost;
77
use sqlbuild_recall::clickhouse::main::error_kind::error_kind;
@@ -85,10 +85,17 @@ fn reply<'py>(
8585
}
8686
}
8787

88-
/// Connect with the store's connection options; a `quiet` caller, Recall's delivery thread, never has its output captured.
88+
/// How a store connection is used: the delivery thread writes and is never captured; a reader skips the session settings under `readonly = 1`, so that user can read.
89+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90+
pub(crate) enum ConnectionRole {
91+
Writer,
92+
Reader,
93+
}
94+
95+
/// Connect with the store's connection options for `role`.
8996
pub(crate) fn connect(
9097
py: Python<'_>,
91-
(config, quiet): (&[(String, ConfigValue)], bool),
98+
(config, role): (&[(String, ConfigValue)], ConnectionRole),
9299
action: &str,
93100
) -> Result<StoreHandle, StoreError> {
94101
let options = PyDict::new(py);
@@ -104,12 +111,18 @@ pub(crate) fn connect(
104111
("config", options.into_any()),
105112
(
106113
"quiet_thread_attribute",
107-
if quiet {
114+
if role == ConnectionRole::Writer {
108115
PyString::new(py, OUTPUT_SUPPRESSED_ATTRIBUTE).into_any()
109116
} else {
110117
py.None().into_bound(py)
111118
},
112119
),
120+
(
121+
"read_only",
122+
PyBool::new(py, role == ConnectionRole::Reader)
123+
.to_owned()
124+
.into_any(),
125+
),
113126
],
114127
action,
115128
)?
@@ -132,11 +145,11 @@ fn cell_value(cell: Option<Cell>) -> RecallValue {
132145
}
133146
}
134147

135-
/// The columns, rows and statistics of `sql` run with `settings`.
148+
/// The columns, at most `row_cap` rows and the statistics of `sql`, run with those of `settings` the session does not already hold.
136149
pub(crate) fn query(
137150
py: Python<'_>,
138151
handle: StoreHandle,
139-
(sql, settings): (&str, &[(&str, i64)]),
152+
(sql, settings, row_cap): (&str, &[(&str, i64)], Option<usize>),
140153
action: &str,
141154
) -> Result<(QueryResult, QueryStatistics), StoreError> {
142155
let query_settings = PyDict::new(py);
@@ -152,6 +165,13 @@ pub(crate) fn query(
152165
("handle", handle_value(py, handle)),
153166
("sql", PyString::new(py, sql).into_any()),
154167
("settings", query_settings.into_any()),
168+
(
169+
"row_cap",
170+
row_cap.map_or_else(
171+
|| py.None().into_bound(py),
172+
|cap| PyInt::new(py, cap).into_any(),
173+
),
174+
),
155175
],
156176
action,
157177
)?;

‎crates/sqlbuild-python/src/bindings/_helpers/recall/clickhouse_store.rs‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@ use sqlbuild_recall::store::models::{
4646
use sqlbuild_recall::store::types::{RecallStore, RecallStoreOpener};
4747

4848
use crate::bindings::_helpers::recall::clickhouse_calls::{
49-
self as calls, QueryStatistics, StoreHandle,
49+
self as calls, ConnectionRole, QueryStatistics, StoreHandle,
50+
};
51+
use crate::bindings::_helpers::recall::constants::{
52+
READ_ONLY_QUERIES, READONLY_SETTING, SECONDS_PER_DAY, UNKNOWN_TABLE_ERROR,
5053
};
51-
use crate::bindings::_helpers::recall::constants::{SECONDS_PER_DAY, UNKNOWN_TABLE_ERROR};
5254

5355
/// Where a ClickHouse store lives and how long it keeps history.
5456
#[derive(Clone, Debug)]
@@ -143,7 +145,7 @@ impl ClickHouseStore {
143145
}
144146

145147
fn query(&self, py: Python<'_>, sql: &str, action: &str) -> Result<QueryResult, StoreError> {
146-
calls::query(py, self.connection()?, (sql, &[]), action).map(|(result, _)| result)
148+
calls::query(py, self.connection()?, (sql, &[], None), action).map(|(result, _)| result)
147149
}
148150

149151
fn command(&self, py: Python<'_>, sql: &str, action: &str) -> Result<(), StoreError> {
@@ -591,7 +593,7 @@ impl RecallStoreOpener for ClickHouseStoreOpener {
591593
location: self.location.clone(),
592594
connection: Some(calls::connect(
593595
py,
594-
(&self.location.config, true),
596+
(&self.location.config, ConnectionRole::Writer),
595597
&self.location.connect_action(),
596598
)?),
597599
columns: HashMap::new(),
@@ -645,7 +647,7 @@ impl ClickHouseReader {
645647
location: location.clone(),
646648
connection: Some(calls::connect(
647649
py,
648-
(&location.config, false),
650+
(&location.config, ConnectionRole::Reader),
649651
&location.connect_action(),
650652
)?),
651653
columns: HashMap::new(),
@@ -683,7 +685,7 @@ impl ClickHouseReader {
683685
location: location.clone(),
684686
connection: Some(calls::connect(
685687
py,
686-
(&location.config, false),
688+
(&location.config, ConnectionRole::Reader),
687689
&location.connect_action(),
688690
)?),
689691
columns: HashMap::new(),
@@ -719,15 +721,13 @@ impl ClickHouseReader {
719721
sql: &str,
720722
limit: Option<usize>,
721723
) -> Result<(QueryResult, QueryStatistics), StoreError> {
722-
let mut settings: Vec<(&str, i64)> = vec![("readonly", 2)];
723-
if let Some(limit) = limit {
724-
settings.push(("limit", i64::try_from(limit).unwrap_or(i64::MAX - 1) + 1));
725-
}
724+
let settings: [(&str, i64); 1] = [(READONLY_SETTING, READ_ONLY_QUERIES)];
725+
let row_cap: Option<usize> = limit.map(|limit| limit.saturating_add(1));
726726
let run = || {
727727
calls::query(
728728
py,
729729
self.store.connection()?,
730-
(sql, &settings),
730+
(sql, &settings, row_cap),
731731
"run the Recall query",
732732
)
733733
};

‎crates/sqlbuild-python/src/bindings/_helpers/recall/constants.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ pub(crate) const CLOSE_POLL_MS: u64 = 50;
100100
pub(crate) const QUARANTINE_START_WAIT_MS: u64 = 200;
101101
/// The ClickHouse error name of a query naming a relation that does not exist yet.
102102
pub(crate) const UNKNOWN_TABLE_ERROR: &str = "UNKNOWN_TABLE";
103+
/// `sqb recall query` runs read-only: the setting, and the value that still allows query settings. Not sent when the session is already read-only.
104+
pub(crate) const READONLY_SETTING: &str = "readonly";
105+
pub(crate) const READ_ONLY_QUERIES: i64 = 2;
103106
/// Seconds in a day, for the artifact refresh cutoff.
104107
pub(crate) const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
105108
/// The parameter a lifecycle-event sink takes its event in.

‎crates/sqlbuild-recall/src/clickhouse/constants.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use crate::schema::models::Table;
44

55
/// Rows one insert batch takes; ClickHouse prefers few large inserts to many small ones.
66
pub const BATCH_ROWS: usize = 5_000;
7-
/// Longest wait for a batch to fill, so each table gets at most about one part per second per writer.
8-
pub const BATCH_WINDOW_MS: u64 = 1_000;
7+
/// Longest wait for a batch to fill, so a long command adds at most one part per table every five seconds. A closing session sends what it holds at once, and heartbeats never wait for it.
8+
pub const BATCH_WINDOW_MS: u64 = 5_000;
99
/// ClickHouse `READONLY`.
1010
pub const READONLY_CODE: u64 = 164;
1111
/// ClickHouse `DATABASE_ACCESS_DENIED`.

0 commit comments

Comments
 (0)