slave_framework/
database.rs1use std::borrow::Cow;
13
14use surrealdb::Surreal;
15use surrealdb::engine::any::Any;
16use surrealdb::method::Query;
17use surrealdb::opt::auth::{Database, Namespace, Root};
18use surrealdb::types::QueryError;
19
20use crate::{Error, Result};
21
22pub mod migrations;
23
24pub type Db = Surreal<Any>;
26
27pub type Transaction = surrealdb::method::Transaction<Any>;
29
30pub trait Executor {
33 fn query<'a>(&'a self, query: impl Into<Cow<'a, str>>) -> Query<'a, Any>;
34}
35
36impl Executor for Db {
37 fn query<'a>(&'a self, query: impl Into<Cow<'a, str>>) -> Query<'a, Any> {
38 Surreal::query(self, query)
39 }
40}
41
42impl Executor for Transaction {
43 fn query<'a>(&'a self, query: impl Into<Cow<'a, str>>) -> Query<'a, Any> {
44 Transaction::query(self, query)
45 }
46}
47
48pub trait MaybeConflict: From<surrealdb::Error> {
51 fn is_transaction_conflict(&self) -> bool;
52}
53
54impl MaybeConflict for surrealdb::Error {
55 fn is_transaction_conflict(&self) -> bool {
56 is_transaction_conflict(self)
57 }
58}
59
60#[doc(hidden)]
63pub async fn finish<T, E: MaybeConflict>(
64 tx: Transaction,
65 result: std::result::Result<T, E>,
66) -> std::result::Result<T, E> {
67 match result {
68 Ok(value) => {
69 tx.commit().await?;
70 Ok(value)
71 }
72 Err(err) => {
73 let _ = tx.cancel().await;
74 Err(err)
75 }
76 }
77}
78
79#[doc(hidden)]
82pub async fn backoff(attempt: usize) {
83 let nanos = std::time::SystemTime::now()
84 .duration_since(std::time::UNIX_EPOCH)
85 .map_or(0, |d| d.subsec_nanos() as u64);
86 let base = 5u64 << attempt.min(6);
87 let millis = base + nanos % base;
88 tokio::time::sleep(std::time::Duration::from_millis(millis)).await;
89}
90
91#[macro_export]
113macro_rules! transaction {
114 ($db:expr, |$tx:ident| $body:block) => {{
115 let db: &$crate::database::Db = $db;
116 let mut attempt = 1;
117 loop {
118 let result = match db.clone().begin().await {
119 Ok($tx) => {
120 let result = async { $body }.await;
121 $crate::database::finish($tx, result).await
122 }
123 Err(err) => Err(err.into()),
124 };
125 match result {
126 Err(err)
127 if $crate::database::MaybeConflict::is_transaction_conflict(&err)
128 && attempt < $crate::database::TRANSACTION_ATTEMPTS =>
129 {
130 attempt += 1;
131 $crate::database::backoff(attempt).await;
132 }
133 result => break result,
134 }
135 }
136 }};
137}
138
139pub const TRANSACTION_ATTEMPTS: usize = 8;
143
144pub const URL_ENV: &str = "SURREAL_URL";
145pub const NAMESPACE_ENV: &str = "SURREAL_NS";
146pub const DATABASE_ENV: &str = "SURREAL_DB";
147pub const USER_ENV: &str = "SURREAL_USER";
148pub const PASSWORD_ENV: &str = "SURREAL_PASS";
149pub const AUTH_LEVEL_ENV: &str = "SURREAL_AUTH_LEVEL";
150
151pub const DEFAULT_URL: &str = "ws://127.0.0.1:8000";
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum AuthLevel {
157 Root,
159 Namespace,
161 Database,
164}
165
166impl std::str::FromStr for AuthLevel {
167 type Err = String;
168
169 fn from_str(value: &str) -> std::result::Result<Self, String> {
170 match value.trim().to_ascii_lowercase().as_str() {
171 "root" => Ok(Self::Root),
172 "namespace" | "ns" => Ok(Self::Namespace),
173 "database" | "db" => Ok(Self::Database),
174 _ => Err("expected `root`, `namespace` or `database`".to_owned()),
175 }
176 }
177}
178
179#[derive(Debug, Clone)]
181pub struct DatabaseConfig {
182 pub url: String,
184 pub namespace: String,
185 pub database: String,
186 pub username: Option<String>,
188 pub password: Option<String>,
189 pub auth_level: AuthLevel,
190}
191
192impl DatabaseConfig {
193 pub fn from_env() -> Result<Self> {
198 let non_empty = |key: &str| std::env::var(key).ok().filter(|v| !v.trim().is_empty());
199 Ok(Self {
200 url: crate::env::var_or(URL_ENV, DEFAULT_URL),
201 namespace: crate::env::var_or(NAMESPACE_ENV, "main"),
202 database: crate::env::var_or(DATABASE_ENV, "main"),
203 username: non_empty(USER_ENV),
204 password: non_empty(PASSWORD_ENV),
205 auth_level: crate::env::parse_or(AUTH_LEVEL_ENV, AuthLevel::Root)?,
206 })
207 }
208
209 pub fn envs(&self) -> Vec<(&'static str, String)> {
211 let level = match self.auth_level {
212 AuthLevel::Root => "root",
213 AuthLevel::Namespace => "namespace",
214 AuthLevel::Database => "database",
215 };
216 let mut envs = vec![
217 (URL_ENV, self.url.clone()),
218 (NAMESPACE_ENV, self.namespace.clone()),
219 (DATABASE_ENV, self.database.clone()),
220 (AUTH_LEVEL_ENV, level.to_owned()),
221 ];
222 envs.extend(self.username.clone().map(|v| (USER_ENV, v)));
223 envs.extend(self.password.clone().map(|v| (PASSWORD_ENV, v)));
224 envs
225 }
226
227 pub fn describe(&self) -> String {
229 format!("{} {}/{}", self.url, self.namespace, self.database)
230 }
231}
232
233pub async fn connect(config: &DatabaseConfig) -> Result<Db> {
235 let db = surrealdb::engine::any::connect(config.url.as_str()).await?;
236 if let Some(username) = &config.username {
237 let password = config
238 .password
239 .clone()
240 .ok_or(Error::MissingDatabaseSetting(PASSWORD_ENV))?;
241 let username = username.clone();
242 match config.auth_level {
243 AuthLevel::Root => {
244 db.signin(Root { username, password }).await?;
245 }
246 AuthLevel::Namespace => {
247 db.signin(Namespace {
248 namespace: config.namespace.clone(),
249 username,
250 password,
251 })
252 .await?;
253 }
254 AuthLevel::Database => {
255 db.signin(Database {
256 namespace: config.namespace.clone(),
257 database: config.database.clone(),
258 username,
259 password,
260 })
261 .await?;
262 }
263 }
264 }
265 db.use_ns(&config.namespace)
266 .use_db(&config.database)
267 .await?;
268 Ok(db)
269}
270
271pub fn is_transaction_conflict(err: &surrealdb::Error) -> bool {
274 matches!(err.query_details(), Some(QueryError::TransactionConflict))
275 || err.message().contains("can be retried")
276}
277
278pub fn is_already_exists(err: &surrealdb::Error) -> bool {
281 err.is_already_exists() || err.message().contains("already contains")
282}