Skip to main content

scopedb_client/
statement.rs

1// Copyright 2024 ScopeDB, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::time::Duration;
16
17use tokio::time::sleep;
18use uuid::Uuid;
19
20use crate::Error;
21use crate::ErrorKind;
22use crate::client::Client;
23use crate::protocol::StatementRequest;
24use crate::protocol::StatementStatus;
25use crate::result::ResultSet;
26
27/// A query with optional execution limits. Use [`Self::execute`] to wait for its
28/// result, or [`Self::submit`] to retain a handle for progress and cancellation.
29#[derive(Debug)]
30pub struct Statement {
31    client: Client,
32    statement: String,
33    statement_id: Option<Uuid>,
34    max_elapsed: Option<Duration>,
35    max_estimated: Option<Duration>,
36}
37
38impl Statement {
39    pub fn with_statement_id(mut self, statement_id: Uuid) -> Self {
40        self.statement_id = Some(statement_id);
41        self
42    }
43
44    /// Limits total elapsed time on the server, including queueing.
45    pub fn with_max_elapsed(mut self, duration: Duration) -> Self {
46        self.max_elapsed = Some(duration);
47        self
48    }
49
50    /// Rejects execution when its estimated duration exceeds this limit.
51    pub fn with_max_estimated(mut self, duration: Duration) -> Self {
52        self.max_estimated = Some(duration);
53        self
54    }
55
56    /// Submits one statement. An interrupted response does not establish whether
57    /// execution started. Supply an ID with [`Self::with_statement_id`] to look it
58    /// up after such a failure without submitting another execution.
59    pub async fn submit(self) -> Result<StatementHandle, Error> {
60        if self.max_elapsed.is_some_and(|d| d.is_zero())
61            || self.max_estimated.is_some_and(|d| d.is_zero())
62        {
63            return Err(Error::new(
64                ErrorKind::ConfigInvalid,
65                "statement duration limits must be positive",
66            ));
67        }
68        let Statement {
69            client,
70            statement,
71            statement_id,
72            max_elapsed,
73            max_estimated,
74        } = self;
75
76        let snapshot = client
77            .submit_statement(StatementRequest {
78                statement,
79                statement_id,
80                max_elapsed_seconds: max_elapsed.map(|v| v.as_secs_f64()),
81                max_estimated_seconds: max_estimated.map(|v| v.as_secs_f64()),
82                format: "json",
83            })
84            .await
85            .map_err(|error| match statement_id {
86                Some(id) => error.set_statement_id(id),
87                None => error,
88            })?;
89
90        Ok(StatementHandle {
91            client,
92            statement_id: snapshot.statement_id(),
93            snapshot: Some(snapshot),
94        })
95    }
96
97    /// Submits and waits. Dropping this future stops polling, not server execution.
98    /// Retain a submitted handle to request cancellation explicitly.
99    /// Errors after submission retain [`Error::statement_id`] and never indicate
100    /// that resubmitting is safe. Resume with [`Client::statement_handle`] instead.
101    pub async fn execute(self) -> Result<ResultSet, Error> {
102        self.submit().await?.wait().await.map_err(|error| {
103            if error.is_retryable() {
104                error.set_persistent()
105            } else {
106                error
107            }
108        })
109    }
110
111    pub(crate) fn new(client: Client, statement: String) -> Self {
112        Self {
113            client,
114            statement,
115            statement_id: None,
116            max_elapsed: None,
117            max_estimated: None,
118        }
119    }
120}
121
122/// Tracks one submitted query. Keep the handle to inspect progress, wait for
123/// results, or request cancellation.
124///
125/// | Operation | Method |
126/// | --------- | ------ |
127/// | Read the last observed status without a request | [`Self::snapshot`] |
128/// | Fetch the current status | [`Self::refresh`] |
129/// | Wait for results | [`Self::wait`] |
130/// | Request cancellation | [`Self::cancel`] |
131///
132/// Dropping a wait or refresh future stops local waiting without cancelling
133/// execution. Terminal statuses are cached, so waiting again returns the result
134/// without another request. Retry temporary read failures on this handle rather
135/// than submitting another query.
136#[derive(Debug)]
137pub struct StatementHandle {
138    client: Client,
139    statement_id: Uuid,
140    snapshot: Option<StatementStatus>,
141}
142
143impl StatementHandle {
144    pub fn statement_id(&self) -> Uuid {
145        self.statement_id
146    }
147
148    /// Returns the status snapshot received by the most recent request without
149    /// making a network request.
150    pub fn snapshot(&self) -> Option<&StatementStatus> {
151        self.snapshot.as_ref()
152    }
153
154    /// Fetches a status snapshot, or returns the cached terminal state.
155    /// Requests on a handle are serialized through its mutable borrow.
156    pub async fn refresh(&mut self) -> Result<&StatementStatus, Error> {
157        if self
158            .snapshot
159            .as_ref()
160            .is_some_and(StatementStatus::is_terminated)
161        {
162            return Ok(self
163                .snapshot
164                .as_ref()
165                .expect("terminal snapshot is present"));
166        }
167
168        self.snapshot = Some(
169            self.client
170                .fetch_statement(self.statement_id)
171                .await
172                .map_err(|error| error.set_statement_id(self.statement_id))?,
173        );
174        Ok(self.snapshot.as_ref().expect("status was just assigned"))
175    }
176
177    /// Waits for the statement to terminate and returns its result set.
178    /// Dropping this future leaves the handle and server execution available.
179    pub async fn wait(&mut self) -> Result<ResultSet, Error> {
180        let mut delay = Duration::from_millis(5);
181        let max_delay = Duration::from_secs(1);
182
183        loop {
184            self.refresh().await?;
185
186            if let Some(status) = self.snapshot.as_ref() {
187                match status {
188                    StatementStatus::Finished(finished) => return Ok(finished.result_set()),
189                    StatementStatus::Failed(failed) => {
190                        return Err(Error::new(
191                            ErrorKind::StatementFailed,
192                            failed.error.message.clone(),
193                        )
194                        .set_statement_id(self.statement_id)
195                        .set_statement_details(failed.error.clone()));
196                    }
197                    StatementStatus::Cancelled(cancelled) => {
198                        return Err(Error::new(
199                            ErrorKind::StatementCancelled,
200                            format!("statement {} was cancelled", cancelled.statement_id),
201                        )
202                        .set_statement_id(self.statement_id));
203                    }
204                    StatementStatus::Pending(..) | StatementStatus::Running(..) => {
205                        sleep(delay).await;
206                        if delay < max_delay {
207                            delay = std::cmp::min(delay.saturating_mul(2), max_delay);
208                        }
209                    }
210                }
211            }
212        }
213    }
214
215    /// Requests cancellation and caches the observed status. Pending or Running
216    /// means cancellation has not yet been observed; call wait() to settle it.
217    /// A completion race returns the full result or original execution failure.
218    pub async fn cancel(&mut self) -> Result<StatementStatus, Error> {
219        if let Some(status) = self.snapshot.as_ref().filter(|s| s.is_terminated()) {
220            return Ok(status.clone());
221        }
222        let status = self
223            .client
224            .cancel_statement(self.statement_id)
225            .await
226            .map_err(|error| error.set_statement_id(self.statement_id))?;
227        self.snapshot = Some(status.clone());
228        Ok(status)
229    }
230
231    pub(crate) fn new(client: Client, statement_id: Uuid) -> Self {
232        Self {
233            client,
234            statement_id,
235            snapshot: None,
236        }
237    }
238}