Line data Source code
1 : pub(crate) mod split_state;
2 : use std::collections::HashMap;
3 : use std::str::FromStr;
4 : use std::time::Duration;
5 : use std::time::Instant;
6 :
7 : use self::split_state::SplitState;
8 : use diesel::pg::PgConnection;
9 : use diesel::prelude::*;
10 : use diesel::Connection;
11 : use itertools::Itertools;
12 : use pageserver_api::controller_api::AvailabilityZone;
13 : use pageserver_api::controller_api::MetadataHealthRecord;
14 : use pageserver_api::controller_api::ShardSchedulingPolicy;
15 : use pageserver_api::controller_api::{NodeSchedulingPolicy, PlacementPolicy};
16 : use pageserver_api::models::TenantConfig;
17 : use pageserver_api::shard::ShardConfigError;
18 : use pageserver_api::shard::ShardIdentity;
19 : use pageserver_api::shard::ShardStripeSize;
20 : use pageserver_api::shard::{ShardCount, ShardNumber, TenantShardId};
21 : use serde::{Deserialize, Serialize};
22 : use utils::generation::Generation;
23 : use utils::id::{NodeId, TenantId};
24 :
25 : use crate::metrics::{
26 : DatabaseQueryErrorLabelGroup, DatabaseQueryLatencyLabelGroup, METRICS_REGISTRY,
27 : };
28 : use crate::node::Node;
29 :
30 : use diesel_migrations::{embed_migrations, EmbeddedMigrations};
31 : const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");
32 :
33 : /// ## What do we store?
34 : ///
35 : /// The storage controller service does not store most of its state durably.
36 : ///
37 : /// The essential things to store durably are:
38 : /// - generation numbers, as these must always advance monotonically to ensure data safety.
39 : /// - Tenant's PlacementPolicy and TenantConfig, as the source of truth for these is something external.
40 : /// - Node's scheduling policies, as the source of truth for these is something external.
41 : ///
42 : /// Other things we store durably as an implementation detail:
43 : /// - Node's host/port: this could be avoided it we made nodes emit a self-registering heartbeat,
44 : /// but it is operationally simpler to make this service the authority for which nodes
45 : /// it talks to.
46 : ///
47 : /// ## Performance/efficiency
48 : ///
49 : /// The storage controller service does not go via the database for most things: there are
50 : /// a couple of places where we must, and where efficiency matters:
51 : /// - Incrementing generation numbers: the Reconciler has to wait for this to complete
52 : /// before it can attach a tenant, so this acts as a bound on how fast things like
53 : /// failover can happen.
54 : /// - Pageserver re-attach: we will increment many shards' generations when this happens,
55 : /// so it is important to avoid e.g. issuing O(N) queries.
56 : ///
57 : /// Database calls relating to nodes have low performance requirements, as they are very rarely
58 : /// updated, and reads of nodes are always from memory, not the database. We only require that
59 : /// we can UPDATE a node's scheduling mode reasonably quickly to mark a bad node offline.
60 : pub struct Persistence {
61 : connection_pool: diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<PgConnection>>,
62 : }
63 :
64 : /// Legacy format, for use in JSON compat objects in test environment
65 0 : #[derive(Serialize, Deserialize)]
66 : struct JsonPersistence {
67 : tenants: HashMap<TenantShardId, TenantShardPersistence>,
68 : }
69 :
70 : #[derive(thiserror::Error, Debug)]
71 : pub(crate) enum DatabaseError {
72 : #[error(transparent)]
73 : Query(#[from] diesel::result::Error),
74 : #[error(transparent)]
75 : Connection(#[from] diesel::result::ConnectionError),
76 : #[error(transparent)]
77 : ConnectionPool(#[from] r2d2::Error),
78 : #[error("Logical error: {0}")]
79 : Logical(String),
80 : #[error("Migration error: {0}")]
81 : Migration(String),
82 : }
83 :
84 : #[derive(measured::FixedCardinalityLabel, Copy, Clone)]
85 : pub(crate) enum DatabaseOperation {
86 : InsertNode,
87 : UpdateNode,
88 : DeleteNode,
89 : ListNodes,
90 : BeginShardSplit,
91 : CompleteShardSplit,
92 : AbortShardSplit,
93 : Detach,
94 : ReAttach,
95 : IncrementGeneration,
96 : TenantGenerations,
97 : ShardGenerations,
98 : ListTenantShards,
99 : InsertTenantShards,
100 : UpdateTenantShard,
101 : DeleteTenant,
102 : UpdateTenantConfig,
103 : UpdateMetadataHealth,
104 : ListMetadataHealth,
105 : ListMetadataHealthUnhealthy,
106 : ListMetadataHealthOutdated,
107 : GetLeader,
108 : UpdateLeader,
109 : SetPreferredAzs,
110 : }
111 :
112 : #[must_use]
113 : pub(crate) enum AbortShardSplitStatus {
114 : /// We aborted the split in the database by reverting to the parent shards
115 : Aborted,
116 : /// The split had already been persisted.
117 : Complete,
118 : }
119 :
120 : pub(crate) type DatabaseResult<T> = Result<T, DatabaseError>;
121 :
122 : /// Some methods can operate on either a whole tenant or a single shard
123 : pub(crate) enum TenantFilter {
124 : Tenant(TenantId),
125 : Shard(TenantShardId),
126 : }
127 :
128 : /// Represents the results of looking up generation+pageserver for the shards of a tenant
129 : pub(crate) struct ShardGenerationState {
130 : pub(crate) tenant_shard_id: TenantShardId,
131 : pub(crate) generation: Option<Generation>,
132 : pub(crate) generation_pageserver: Option<NodeId>,
133 : }
134 :
135 : impl Persistence {
136 : // The default postgres connection limit is 100. We use up to 99, to leave one free for a human admin under
137 : // normal circumstances. This assumes we have exclusive use of the database cluster to which we connect.
138 : pub const MAX_CONNECTIONS: u32 = 99;
139 :
140 : // We don't want to keep a lot of connections alive: close them down promptly if they aren't being used.
141 : const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(10);
142 : const MAX_CONNECTION_LIFETIME: Duration = Duration::from_secs(60);
143 :
144 0 : pub fn new(database_url: String) -> Self {
145 0 : let manager = diesel::r2d2::ConnectionManager::<PgConnection>::new(database_url);
146 0 :
147 0 : // We will use a connection pool: this is primarily to _limit_ our connection count, rather than to optimize time
148 0 : // to execute queries (database queries are not generally on latency-sensitive paths).
149 0 : let connection_pool = diesel::r2d2::Pool::builder()
150 0 : .max_size(Self::MAX_CONNECTIONS)
151 0 : .max_lifetime(Some(Self::MAX_CONNECTION_LIFETIME))
152 0 : .idle_timeout(Some(Self::IDLE_CONNECTION_TIMEOUT))
153 0 : // Always keep at least one connection ready to go
154 0 : .min_idle(Some(1))
155 0 : .test_on_check_out(true)
156 0 : .build(manager)
157 0 : .expect("Could not build connection pool");
158 0 :
159 0 : Self { connection_pool }
160 0 : }
161 :
162 : /// A helper for use during startup, where we would like to tolerate concurrent restarts of the
163 : /// database and the storage controller, therefore the database might not be available right away
164 0 : pub async fn await_connection(
165 0 : database_url: &str,
166 0 : timeout: Duration,
167 0 : ) -> Result<(), diesel::ConnectionError> {
168 0 : let started_at = Instant::now();
169 : loop {
170 0 : match PgConnection::establish(database_url) {
171 : Ok(_) => {
172 0 : tracing::info!("Connected to database.");
173 0 : return Ok(());
174 : }
175 0 : Err(e) => {
176 0 : if started_at.elapsed() > timeout {
177 0 : return Err(e);
178 : } else {
179 0 : tracing::info!("Database not yet available, waiting... ({e})");
180 0 : tokio::time::sleep(Duration::from_millis(100)).await;
181 : }
182 : }
183 : }
184 : }
185 0 : }
186 :
187 : /// Execute the diesel migrations that are built into this binary
188 0 : pub(crate) async fn migration_run(&self) -> DatabaseResult<()> {
189 : use diesel_migrations::{HarnessWithOutput, MigrationHarness};
190 :
191 0 : self.with_conn(move |conn| -> DatabaseResult<()> {
192 0 : HarnessWithOutput::write_to_stdout(conn)
193 0 : .run_pending_migrations(MIGRATIONS)
194 0 : .map(|_| ())
195 0 : .map_err(|e| DatabaseError::Migration(e.to_string()))
196 0 : })
197 0 : .await
198 0 : }
199 :
200 : /// Wraps `with_conn` in order to collect latency and error metrics
201 0 : async fn with_measured_conn<F, R>(&self, op: DatabaseOperation, func: F) -> DatabaseResult<R>
202 0 : where
203 0 : F: Fn(&mut PgConnection) -> DatabaseResult<R> + Send + 'static,
204 0 : R: Send + 'static,
205 0 : {
206 0 : let latency = &METRICS_REGISTRY
207 0 : .metrics_group
208 0 : .storage_controller_database_query_latency;
209 0 : let _timer = latency.start_timer(DatabaseQueryLatencyLabelGroup { operation: op });
210 :
211 0 : let res = self.with_conn(func).await;
212 :
213 0 : if let Err(err) = &res {
214 0 : let error_counter = &METRICS_REGISTRY
215 0 : .metrics_group
216 0 : .storage_controller_database_query_error;
217 0 : error_counter.inc(DatabaseQueryErrorLabelGroup {
218 0 : error_type: err.error_label(),
219 0 : operation: op,
220 0 : })
221 0 : }
222 :
223 0 : res
224 0 : }
225 :
226 : /// Call the provided function in a tokio blocking thread, with a Diesel database connection.
227 0 : async fn with_conn<F, R>(&self, func: F) -> DatabaseResult<R>
228 0 : where
229 0 : F: Fn(&mut PgConnection) -> DatabaseResult<R> + Send + 'static,
230 0 : R: Send + 'static,
231 0 : {
232 : // A generous allowance for how many times we may retry serializable transactions
233 : // before giving up. This is not expected to be hit: it is a defensive measure in case we
234 : // somehow engineer a situation where duelling transactions might otherwise live-lock.
235 : const MAX_RETRIES: usize = 128;
236 :
237 0 : let mut conn = self.connection_pool.get()?;
238 0 : tokio::task::spawn_blocking(move || -> DatabaseResult<R> {
239 0 : let mut retry_count = 0;
240 : loop {
241 0 : match conn.build_transaction().serializable().run(|c| func(c)) {
242 0 : Ok(r) => break Ok(r),
243 : Err(
244 0 : err @ DatabaseError::Query(diesel::result::Error::DatabaseError(
245 0 : diesel::result::DatabaseErrorKind::SerializationFailure,
246 0 : _,
247 0 : )),
248 0 : ) => {
249 0 : retry_count += 1;
250 0 : if retry_count > MAX_RETRIES {
251 0 : tracing::error!(
252 0 : "Exceeded max retries on SerializationFailure errors: {err:?}"
253 : );
254 0 : break Err(err);
255 : } else {
256 : // Retry on serialization errors: these are expected, because even though our
257 : // transactions don't fight for the same rows, they will occasionally collide
258 : // on index pages (e.g. increment_generation for unrelated shards can collide)
259 0 : tracing::debug!(
260 0 : "Retrying transaction on serialization failure {err:?}"
261 : );
262 0 : continue;
263 : }
264 : }
265 0 : Err(e) => break Err(e),
266 : }
267 : }
268 0 : })
269 0 : .await
270 0 : .expect("Task panic")
271 0 : }
272 :
273 : /// When a node is first registered, persist it before using it for anything
274 0 : pub(crate) async fn insert_node(&self, node: &Node) -> DatabaseResult<()> {
275 0 : let np = node.to_persistent();
276 0 : self.with_measured_conn(
277 0 : DatabaseOperation::InsertNode,
278 0 : move |conn| -> DatabaseResult<()> {
279 0 : diesel::insert_into(crate::schema::nodes::table)
280 0 : .values(&np)
281 0 : .execute(conn)?;
282 0 : Ok(())
283 0 : },
284 0 : )
285 0 : .await
286 0 : }
287 :
288 : /// At startup, populate the list of nodes which our shards may be placed on
289 0 : pub(crate) async fn list_nodes(&self) -> DatabaseResult<Vec<NodePersistence>> {
290 0 : let nodes: Vec<NodePersistence> = self
291 0 : .with_measured_conn(
292 0 : DatabaseOperation::ListNodes,
293 0 : move |conn| -> DatabaseResult<_> {
294 0 : Ok(crate::schema::nodes::table.load::<NodePersistence>(conn)?)
295 0 : },
296 0 : )
297 0 : .await?;
298 :
299 0 : tracing::info!("list_nodes: loaded {} nodes", nodes.len());
300 :
301 0 : Ok(nodes)
302 0 : }
303 :
304 0 : pub(crate) async fn update_node(
305 0 : &self,
306 0 : input_node_id: NodeId,
307 0 : input_scheduling: NodeSchedulingPolicy,
308 0 : ) -> DatabaseResult<()> {
309 : use crate::schema::nodes::dsl::*;
310 0 : let updated = self
311 0 : .with_measured_conn(DatabaseOperation::UpdateNode, move |conn| {
312 0 : let updated = diesel::update(nodes)
313 0 : .filter(node_id.eq(input_node_id.0 as i64))
314 0 : .set((scheduling_policy.eq(String::from(input_scheduling)),))
315 0 : .execute(conn)?;
316 0 : Ok(updated)
317 0 : })
318 0 : .await?;
319 :
320 0 : if updated != 1 {
321 0 : Err(DatabaseError::Logical(format!(
322 0 : "Node {node_id:?} not found for update",
323 0 : )))
324 : } else {
325 0 : Ok(())
326 : }
327 0 : }
328 :
329 : /// At startup, load the high level state for shards, such as their config + policy. This will
330 : /// be enriched at runtime with state discovered on pageservers.
331 0 : pub(crate) async fn list_tenant_shards(&self) -> DatabaseResult<Vec<TenantShardPersistence>> {
332 0 : self.with_measured_conn(
333 0 : DatabaseOperation::ListTenantShards,
334 0 : move |conn| -> DatabaseResult<_> {
335 0 : Ok(crate::schema::tenant_shards::table.load::<TenantShardPersistence>(conn)?)
336 0 : },
337 0 : )
338 0 : .await
339 0 : }
340 :
341 : /// Tenants must be persisted before we schedule them for the first time. This enables us
342 : /// to correctly retain generation monotonicity, and the externally provided placement policy & config.
343 0 : pub(crate) async fn insert_tenant_shards(
344 0 : &self,
345 0 : shards: Vec<TenantShardPersistence>,
346 0 : ) -> DatabaseResult<()> {
347 : use crate::schema::metadata_health;
348 : use crate::schema::tenant_shards;
349 :
350 0 : let now = chrono::Utc::now();
351 0 :
352 0 : let metadata_health_records = shards
353 0 : .iter()
354 0 : .map(|t| MetadataHealthPersistence {
355 0 : tenant_id: t.tenant_id.clone(),
356 0 : shard_number: t.shard_number,
357 0 : shard_count: t.shard_count,
358 0 : healthy: true,
359 0 : last_scrubbed_at: now,
360 0 : })
361 0 : .collect::<Vec<_>>();
362 0 :
363 0 : self.with_measured_conn(
364 0 : DatabaseOperation::InsertTenantShards,
365 0 : move |conn| -> DatabaseResult<()> {
366 0 : diesel::insert_into(tenant_shards::table)
367 0 : .values(&shards)
368 0 : .execute(conn)?;
369 :
370 0 : diesel::insert_into(metadata_health::table)
371 0 : .values(&metadata_health_records)
372 0 : .execute(conn)?;
373 0 : Ok(())
374 0 : },
375 0 : )
376 0 : .await
377 0 : }
378 :
379 : /// Ordering: call this _after_ deleting the tenant on pageservers, but _before_ dropping state for
380 : /// the tenant from memory on this server.
381 0 : pub(crate) async fn delete_tenant(&self, del_tenant_id: TenantId) -> DatabaseResult<()> {
382 : use crate::schema::tenant_shards::dsl::*;
383 0 : self.with_measured_conn(
384 0 : DatabaseOperation::DeleteTenant,
385 0 : move |conn| -> DatabaseResult<()> {
386 0 : // `metadata_health` status (if exists) is also deleted based on the cascade behavior.
387 0 : diesel::delete(tenant_shards)
388 0 : .filter(tenant_id.eq(del_tenant_id.to_string()))
389 0 : .execute(conn)?;
390 0 : Ok(())
391 0 : },
392 0 : )
393 0 : .await
394 0 : }
395 :
396 0 : pub(crate) async fn delete_node(&self, del_node_id: NodeId) -> DatabaseResult<()> {
397 : use crate::schema::nodes::dsl::*;
398 0 : self.with_measured_conn(
399 0 : DatabaseOperation::DeleteNode,
400 0 : move |conn| -> DatabaseResult<()> {
401 0 : diesel::delete(nodes)
402 0 : .filter(node_id.eq(del_node_id.0 as i64))
403 0 : .execute(conn)?;
404 :
405 0 : Ok(())
406 0 : },
407 0 : )
408 0 : .await
409 0 : }
410 :
411 : /// When a tenant invokes the /re-attach API, this function is responsible for doing an efficient
412 : /// batched increment of the generations of all tenants whose generation_pageserver is equal to
413 : /// the node that called /re-attach.
414 0 : #[tracing::instrument(skip_all, fields(node_id))]
415 : pub(crate) async fn re_attach(
416 : &self,
417 : input_node_id: NodeId,
418 : ) -> DatabaseResult<HashMap<TenantShardId, Generation>> {
419 : use crate::schema::nodes::dsl::scheduling_policy;
420 : use crate::schema::nodes::dsl::*;
421 : use crate::schema::tenant_shards::dsl::*;
422 : let updated = self
423 0 : .with_measured_conn(DatabaseOperation::ReAttach, move |conn| {
424 0 : let rows_updated = diesel::update(tenant_shards)
425 0 : .filter(generation_pageserver.eq(input_node_id.0 as i64))
426 0 : .set(generation.eq(generation + 1))
427 0 : .execute(conn)?;
428 :
429 0 : tracing::info!("Incremented {} tenants' generations", rows_updated);
430 :
431 : // TODO: UPDATE+SELECT in one query
432 :
433 0 : let updated = tenant_shards
434 0 : .filter(generation_pageserver.eq(input_node_id.0 as i64))
435 0 : .select(TenantShardPersistence::as_select())
436 0 : .load(conn)?;
437 :
438 : // If the node went through a drain and restart phase before re-attaching,
439 : // then reset it's node scheduling policy to active.
440 0 : diesel::update(nodes)
441 0 : .filter(node_id.eq(input_node_id.0 as i64))
442 0 : .filter(
443 0 : scheduling_policy
444 0 : .eq(String::from(NodeSchedulingPolicy::PauseForRestart))
445 0 : .or(scheduling_policy.eq(String::from(NodeSchedulingPolicy::Draining)))
446 0 : .or(scheduling_policy.eq(String::from(NodeSchedulingPolicy::Filling))),
447 0 : )
448 0 : .set(scheduling_policy.eq(String::from(NodeSchedulingPolicy::Active)))
449 0 : .execute(conn)?;
450 :
451 0 : Ok(updated)
452 0 : })
453 : .await?;
454 :
455 : let mut result = HashMap::new();
456 : for tsp in updated {
457 : let tenant_shard_id = TenantShardId {
458 : tenant_id: TenantId::from_str(tsp.tenant_id.as_str())
459 0 : .map_err(|e| DatabaseError::Logical(format!("Malformed tenant id: {e}")))?,
460 : shard_number: ShardNumber(tsp.shard_number as u8),
461 : shard_count: ShardCount::new(tsp.shard_count as u8),
462 : };
463 :
464 : let Some(g) = tsp.generation else {
465 : // If the generation_pageserver column was non-NULL, then the generation column should also be non-NULL:
466 : // we only set generation_pageserver when setting generation.
467 : return Err(DatabaseError::Logical(
468 : "Generation should always be set after incrementing".to_string(),
469 : ));
470 : };
471 : result.insert(tenant_shard_id, Generation::new(g as u32));
472 : }
473 :
474 : Ok(result)
475 : }
476 :
477 : /// Reconciler calls this immediately before attaching to a new pageserver, to acquire a unique, monotonically
478 : /// advancing generation number. We also store the NodeId for which the generation was issued, so that in
479 : /// [`Self::re_attach`] we can do a bulk UPDATE on the generations for that node.
480 0 : pub(crate) async fn increment_generation(
481 0 : &self,
482 0 : tenant_shard_id: TenantShardId,
483 0 : node_id: NodeId,
484 0 : ) -> anyhow::Result<Generation> {
485 : use crate::schema::tenant_shards::dsl::*;
486 0 : let updated = self
487 0 : .with_measured_conn(DatabaseOperation::IncrementGeneration, move |conn| {
488 0 : let updated = diesel::update(tenant_shards)
489 0 : .filter(tenant_id.eq(tenant_shard_id.tenant_id.to_string()))
490 0 : .filter(shard_number.eq(tenant_shard_id.shard_number.0 as i32))
491 0 : .filter(shard_count.eq(tenant_shard_id.shard_count.literal() as i32))
492 0 : .set((
493 0 : generation.eq(generation + 1),
494 0 : generation_pageserver.eq(node_id.0 as i64),
495 0 : ))
496 0 : // TODO: only returning() the generation column
497 0 : .returning(TenantShardPersistence::as_returning())
498 0 : .get_result(conn)?;
499 :
500 0 : Ok(updated)
501 0 : })
502 0 : .await?;
503 :
504 : // Generation is always non-null in the rseult: if the generation column had been NULL, then we
505 : // should have experienced an SQL Confilict error while executing a query that tries to increment it.
506 0 : debug_assert!(updated.generation.is_some());
507 0 : let Some(g) = updated.generation else {
508 0 : return Err(DatabaseError::Logical(
509 0 : "Generation should always be set after incrementing".to_string(),
510 0 : )
511 0 : .into());
512 : };
513 :
514 0 : Ok(Generation::new(g as u32))
515 0 : }
516 :
517 : /// When we want to call out to the running shards for a tenant, e.g. during timeline CRUD operations,
518 : /// we need to know where the shard is attached, _and_ the generation, so that we can re-check the generation
519 : /// afterwards to confirm that our timeline CRUD operation is truly persistent (it must have happened in the
520 : /// latest generation)
521 : ///
522 : /// If the tenant doesn't exist, an empty vector is returned.
523 : ///
524 : /// Output is sorted by shard number
525 0 : pub(crate) async fn tenant_generations(
526 0 : &self,
527 0 : filter_tenant_id: TenantId,
528 0 : ) -> Result<Vec<ShardGenerationState>, DatabaseError> {
529 : use crate::schema::tenant_shards::dsl::*;
530 0 : let rows = self
531 0 : .with_measured_conn(DatabaseOperation::TenantGenerations, move |conn| {
532 0 : let result = tenant_shards
533 0 : .filter(tenant_id.eq(filter_tenant_id.to_string()))
534 0 : .select(TenantShardPersistence::as_select())
535 0 : .order(shard_number)
536 0 : .load(conn)?;
537 0 : Ok(result)
538 0 : })
539 0 : .await?;
540 :
541 0 : Ok(rows
542 0 : .into_iter()
543 0 : .map(|p| ShardGenerationState {
544 0 : tenant_shard_id: p
545 0 : .get_tenant_shard_id()
546 0 : .expect("Corrupt tenant shard id in database"),
547 0 : generation: p.generation.map(|g| Generation::new(g as u32)),
548 0 : generation_pageserver: p.generation_pageserver.map(|n| NodeId(n as u64)),
549 0 : })
550 0 : .collect())
551 0 : }
552 :
553 : /// Read the generation number of specific tenant shards
554 : ///
555 : /// Output is unsorted. Output may not include values for all inputs, if they are missing in the database.
556 0 : pub(crate) async fn shard_generations(
557 0 : &self,
558 0 : mut tenant_shard_ids: impl Iterator<Item = &TenantShardId>,
559 0 : ) -> Result<Vec<(TenantShardId, Option<Generation>)>, DatabaseError> {
560 0 : let mut rows = Vec::with_capacity(tenant_shard_ids.size_hint().0);
561 :
562 : // We will chunk our input to avoid composing arbitrarily long `IN` clauses. Typically we are
563 : // called with a single digit number of IDs, but in principle we could be called with tens
564 : // of thousands (all the shards on one pageserver) from the generation validation API.
565 0 : loop {
566 0 : // A modest hardcoded chunk size to handle typical cases in a single query but never generate particularly
567 0 : // large query strings.
568 0 : let chunk_ids = tenant_shard_ids.by_ref().take(32);
569 0 :
570 0 : // Compose a comma separated list of tuples for matching on (tenant_id, shard_number, shard_count)
571 0 : let in_clause = chunk_ids
572 0 : .map(|tsid| {
573 0 : format!(
574 0 : "('{}', {}, {})",
575 0 : tsid.tenant_id, tsid.shard_number.0, tsid.shard_count.0
576 0 : )
577 0 : })
578 0 : .join(",");
579 0 :
580 0 : // We are done when our iterator gives us nothing to filter on
581 0 : if in_clause.is_empty() {
582 0 : break;
583 0 : }
584 :
585 0 : let chunk_rows = self
586 0 : .with_measured_conn(DatabaseOperation::ShardGenerations, move |conn| {
587 : // diesel doesn't support multi-column IN queries, so we compose raw SQL. No escaping is required because
588 : // the inputs are strongly typed and cannot carry any user-supplied raw string content.
589 0 : let result : Vec<TenantShardPersistence> = diesel::sql_query(
590 0 : format!("SELECT * from tenant_shards where (tenant_id, shard_number, shard_count) in ({in_clause});").as_str()
591 0 : ).load(conn)?;
592 :
593 0 : Ok(result)
594 0 : })
595 0 : .await?;
596 0 : rows.extend(chunk_rows.into_iter())
597 : }
598 :
599 0 : Ok(rows
600 0 : .into_iter()
601 0 : .map(|tsp| {
602 0 : (
603 0 : tsp.get_tenant_shard_id()
604 0 : .expect("Bad tenant ID in database"),
605 0 : tsp.generation.map(|g| Generation::new(g as u32)),
606 0 : )
607 0 : })
608 0 : .collect())
609 0 : }
610 :
611 : #[allow(non_local_definitions)]
612 : /// For use when updating a persistent property of a tenant, such as its config or placement_policy.
613 : ///
614 : /// Do not use this for settting generation, unless in the special onboarding code path (/location_config)
615 : /// API: use [`Self::increment_generation`] instead. Setting the generation via this route is a one-time thing
616 : /// that we only do the first time a tenant is set to an attached policy via /location_config.
617 0 : pub(crate) async fn update_tenant_shard(
618 0 : &self,
619 0 : tenant: TenantFilter,
620 0 : input_placement_policy: Option<PlacementPolicy>,
621 0 : input_config: Option<TenantConfig>,
622 0 : input_generation: Option<Generation>,
623 0 : input_scheduling_policy: Option<ShardSchedulingPolicy>,
624 0 : ) -> DatabaseResult<()> {
625 : use crate::schema::tenant_shards::dsl::*;
626 :
627 0 : self.with_measured_conn(DatabaseOperation::UpdateTenantShard, move |conn| {
628 0 : let query = match tenant {
629 0 : TenantFilter::Shard(tenant_shard_id) => diesel::update(tenant_shards)
630 0 : .filter(tenant_id.eq(tenant_shard_id.tenant_id.to_string()))
631 0 : .filter(shard_number.eq(tenant_shard_id.shard_number.0 as i32))
632 0 : .filter(shard_count.eq(tenant_shard_id.shard_count.literal() as i32))
633 0 : .into_boxed(),
634 0 : TenantFilter::Tenant(input_tenant_id) => diesel::update(tenant_shards)
635 0 : .filter(tenant_id.eq(input_tenant_id.to_string()))
636 0 : .into_boxed(),
637 : };
638 :
639 : // Clear generation_pageserver if we are moving into a state where we won't have
640 : // any attached pageservers.
641 0 : let input_generation_pageserver = match input_placement_policy {
642 0 : None | Some(PlacementPolicy::Attached(_)) => None,
643 0 : Some(PlacementPolicy::Detached | PlacementPolicy::Secondary) => Some(None),
644 : };
645 :
646 0 : #[derive(AsChangeset)]
647 : #[diesel(table_name = crate::schema::tenant_shards)]
648 : struct ShardUpdate {
649 : generation: Option<i32>,
650 : placement_policy: Option<String>,
651 : config: Option<String>,
652 : scheduling_policy: Option<String>,
653 : generation_pageserver: Option<Option<i64>>,
654 : }
655 :
656 0 : let update = ShardUpdate {
657 0 : generation: input_generation.map(|g| g.into().unwrap() as i32),
658 0 : placement_policy: input_placement_policy
659 0 : .as_ref()
660 0 : .map(|p| serde_json::to_string(&p).unwrap()),
661 0 : config: input_config
662 0 : .as_ref()
663 0 : .map(|c| serde_json::to_string(&c).unwrap()),
664 0 : scheduling_policy: input_scheduling_policy
665 0 : .map(|p| serde_json::to_string(&p).unwrap()),
666 0 : generation_pageserver: input_generation_pageserver,
667 0 : };
668 0 :
669 0 : query.set(update).execute(conn)?;
670 :
671 0 : Ok(())
672 0 : })
673 0 : .await?;
674 :
675 0 : Ok(())
676 0 : }
677 :
678 0 : pub(crate) async fn set_tenant_shard_preferred_azs(
679 0 : &self,
680 0 : preferred_azs: Vec<(TenantShardId, AvailabilityZone)>,
681 0 : ) -> DatabaseResult<Vec<(TenantShardId, AvailabilityZone)>> {
682 : use crate::schema::tenant_shards::dsl::*;
683 :
684 0 : self.with_measured_conn(DatabaseOperation::SetPreferredAzs, move |conn| {
685 0 : let mut shards_updated = Vec::default();
686 :
687 0 : for (tenant_shard_id, preferred_az) in preferred_azs.iter() {
688 0 : let updated = diesel::update(tenant_shards)
689 0 : .filter(tenant_id.eq(tenant_shard_id.tenant_id.to_string()))
690 0 : .filter(shard_number.eq(tenant_shard_id.shard_number.0 as i32))
691 0 : .filter(shard_count.eq(tenant_shard_id.shard_count.literal() as i32))
692 0 : .set(preferred_az_id.eq(preferred_az.0.clone()))
693 0 : .execute(conn)?;
694 :
695 0 : if updated == 1 {
696 0 : shards_updated.push((*tenant_shard_id, preferred_az.clone()));
697 0 : }
698 : }
699 :
700 0 : Ok(shards_updated)
701 0 : })
702 0 : .await
703 0 : }
704 :
705 0 : pub(crate) async fn detach(&self, tenant_shard_id: TenantShardId) -> anyhow::Result<()> {
706 : use crate::schema::tenant_shards::dsl::*;
707 0 : self.with_measured_conn(DatabaseOperation::Detach, move |conn| {
708 0 : let updated = diesel::update(tenant_shards)
709 0 : .filter(tenant_id.eq(tenant_shard_id.tenant_id.to_string()))
710 0 : .filter(shard_number.eq(tenant_shard_id.shard_number.0 as i32))
711 0 : .filter(shard_count.eq(tenant_shard_id.shard_count.literal() as i32))
712 0 : .set((
713 0 : generation_pageserver.eq(Option::<i64>::None),
714 0 : placement_policy.eq(serde_json::to_string(&PlacementPolicy::Detached).unwrap()),
715 0 : ))
716 0 : .execute(conn)?;
717 :
718 0 : Ok(updated)
719 0 : })
720 0 : .await?;
721 :
722 0 : Ok(())
723 0 : }
724 :
725 : // When we start shard splitting, we must durably mark the tenant so that
726 : // on restart, we know that we must go through recovery.
727 : //
728 : // We create the child shards here, so that they will be available for increment_generation calls
729 : // if some pageserver holding a child shard needs to restart before the overall tenant split is complete.
730 0 : pub(crate) async fn begin_shard_split(
731 0 : &self,
732 0 : old_shard_count: ShardCount,
733 0 : split_tenant_id: TenantId,
734 0 : parent_to_children: Vec<(TenantShardId, Vec<TenantShardPersistence>)>,
735 0 : ) -> DatabaseResult<()> {
736 : use crate::schema::tenant_shards::dsl::*;
737 0 : self.with_measured_conn(DatabaseOperation::BeginShardSplit, move |conn| -> DatabaseResult<()> {
738 : // Mark parent shards as splitting
739 :
740 0 : let updated = diesel::update(tenant_shards)
741 0 : .filter(tenant_id.eq(split_tenant_id.to_string()))
742 0 : .filter(shard_count.eq(old_shard_count.literal() as i32))
743 0 : .set((splitting.eq(1),))
744 0 : .execute(conn)?;
745 0 : if u8::try_from(updated)
746 0 : .map_err(|_| DatabaseError::Logical(
747 0 : format!("Overflow existing shard count {} while splitting", updated))
748 0 : )? != old_shard_count.count() {
749 : // Perhaps a deletion or another split raced with this attempt to split, mutating
750 : // the parent shards that we intend to split. In this case the split request should fail.
751 0 : return Err(DatabaseError::Logical(
752 0 : format!("Unexpected existing shard count {updated} when preparing tenant for split (expected {})", old_shard_count.count())
753 0 : ));
754 0 : }
755 0 :
756 0 : // FIXME: spurious clone to sidestep closure move rules
757 0 : let parent_to_children = parent_to_children.clone();
758 :
759 : // Insert child shards
760 0 : for (parent_shard_id, children) in parent_to_children {
761 0 : let mut parent = crate::schema::tenant_shards::table
762 0 : .filter(tenant_id.eq(parent_shard_id.tenant_id.to_string()))
763 0 : .filter(shard_number.eq(parent_shard_id.shard_number.0 as i32))
764 0 : .filter(shard_count.eq(parent_shard_id.shard_count.literal() as i32))
765 0 : .load::<TenantShardPersistence>(conn)?;
766 0 : let parent = if parent.len() != 1 {
767 0 : return Err(DatabaseError::Logical(format!(
768 0 : "Parent shard {parent_shard_id} not found"
769 0 : )));
770 : } else {
771 0 : parent.pop().unwrap()
772 : };
773 0 : for mut shard in children {
774 : // Carry the parent's generation into the child
775 0 : shard.generation = parent.generation;
776 0 :
777 0 : debug_assert!(shard.splitting == SplitState::Splitting);
778 0 : diesel::insert_into(tenant_shards)
779 0 : .values(shard)
780 0 : .execute(conn)?;
781 : }
782 : }
783 :
784 0 : Ok(())
785 0 : })
786 0 : .await
787 0 : }
788 :
789 : // When we finish shard splitting, we must atomically clean up the old shards
790 : // and insert the new shards, and clear the splitting marker.
791 0 : pub(crate) async fn complete_shard_split(
792 0 : &self,
793 0 : split_tenant_id: TenantId,
794 0 : old_shard_count: ShardCount,
795 0 : ) -> DatabaseResult<()> {
796 : use crate::schema::tenant_shards::dsl::*;
797 0 : self.with_measured_conn(
798 0 : DatabaseOperation::CompleteShardSplit,
799 0 : move |conn| -> DatabaseResult<()> {
800 0 : // Drop parent shards
801 0 : diesel::delete(tenant_shards)
802 0 : .filter(tenant_id.eq(split_tenant_id.to_string()))
803 0 : .filter(shard_count.eq(old_shard_count.literal() as i32))
804 0 : .execute(conn)?;
805 :
806 : // Clear sharding flag
807 0 : let updated = diesel::update(tenant_shards)
808 0 : .filter(tenant_id.eq(split_tenant_id.to_string()))
809 0 : .set((splitting.eq(0),))
810 0 : .execute(conn)?;
811 0 : debug_assert!(updated > 0);
812 :
813 0 : Ok(())
814 0 : },
815 0 : )
816 0 : .await
817 0 : }
818 :
819 : /// Used when the remote part of a shard split failed: we will revert the database state to have only
820 : /// the parent shards, with SplitState::Idle.
821 0 : pub(crate) async fn abort_shard_split(
822 0 : &self,
823 0 : split_tenant_id: TenantId,
824 0 : new_shard_count: ShardCount,
825 0 : ) -> DatabaseResult<AbortShardSplitStatus> {
826 : use crate::schema::tenant_shards::dsl::*;
827 0 : self.with_measured_conn(
828 0 : DatabaseOperation::AbortShardSplit,
829 0 : move |conn| -> DatabaseResult<AbortShardSplitStatus> {
830 : // Clear the splitting state on parent shards
831 0 : let updated = diesel::update(tenant_shards)
832 0 : .filter(tenant_id.eq(split_tenant_id.to_string()))
833 0 : .filter(shard_count.ne(new_shard_count.literal() as i32))
834 0 : .set((splitting.eq(0),))
835 0 : .execute(conn)?;
836 :
837 : // Parent shards are already gone: we cannot abort.
838 0 : if updated == 0 {
839 0 : return Ok(AbortShardSplitStatus::Complete);
840 0 : }
841 0 :
842 0 : // Sanity check: if parent shards were present, their cardinality should
843 0 : // be less than the number of child shards.
844 0 : if updated >= new_shard_count.count() as usize {
845 0 : return Err(DatabaseError::Logical(format!(
846 0 : "Unexpected parent shard count {updated} while aborting split to \
847 0 : count {new_shard_count:?} on tenant {split_tenant_id}"
848 0 : )));
849 0 : }
850 0 :
851 0 : // Erase child shards
852 0 : diesel::delete(tenant_shards)
853 0 : .filter(tenant_id.eq(split_tenant_id.to_string()))
854 0 : .filter(shard_count.eq(new_shard_count.literal() as i32))
855 0 : .execute(conn)?;
856 :
857 0 : Ok(AbortShardSplitStatus::Aborted)
858 0 : },
859 0 : )
860 0 : .await
861 0 : }
862 :
863 : /// Stores all the latest metadata health updates durably. Updates existing entry on conflict.
864 : ///
865 : /// **Correctness:** `metadata_health_updates` should all belong the tenant shards managed by the storage controller.
866 : #[allow(dead_code)]
867 0 : pub(crate) async fn update_metadata_health_records(
868 0 : &self,
869 0 : healthy_records: Vec<MetadataHealthPersistence>,
870 0 : unhealthy_records: Vec<MetadataHealthPersistence>,
871 0 : now: chrono::DateTime<chrono::Utc>,
872 0 : ) -> DatabaseResult<()> {
873 : use crate::schema::metadata_health::dsl::*;
874 :
875 0 : self.with_measured_conn(
876 0 : DatabaseOperation::UpdateMetadataHealth,
877 0 : move |conn| -> DatabaseResult<_> {
878 0 : diesel::insert_into(metadata_health)
879 0 : .values(&healthy_records)
880 0 : .on_conflict((tenant_id, shard_number, shard_count))
881 0 : .do_update()
882 0 : .set((healthy.eq(true), last_scrubbed_at.eq(now)))
883 0 : .execute(conn)?;
884 :
885 0 : diesel::insert_into(metadata_health)
886 0 : .values(&unhealthy_records)
887 0 : .on_conflict((tenant_id, shard_number, shard_count))
888 0 : .do_update()
889 0 : .set((healthy.eq(false), last_scrubbed_at.eq(now)))
890 0 : .execute(conn)?;
891 0 : Ok(())
892 0 : },
893 0 : )
894 0 : .await
895 0 : }
896 :
897 : /// Lists all the metadata health records.
898 : #[allow(dead_code)]
899 0 : pub(crate) async fn list_metadata_health_records(
900 0 : &self,
901 0 : ) -> DatabaseResult<Vec<MetadataHealthPersistence>> {
902 0 : self.with_measured_conn(
903 0 : DatabaseOperation::ListMetadataHealth,
904 0 : move |conn| -> DatabaseResult<_> {
905 0 : Ok(
906 0 : crate::schema::metadata_health::table
907 0 : .load::<MetadataHealthPersistence>(conn)?,
908 : )
909 0 : },
910 0 : )
911 0 : .await
912 0 : }
913 :
914 : /// Lists all the metadata health records that is unhealthy.
915 : #[allow(dead_code)]
916 0 : pub(crate) async fn list_unhealthy_metadata_health_records(
917 0 : &self,
918 0 : ) -> DatabaseResult<Vec<MetadataHealthPersistence>> {
919 : use crate::schema::metadata_health::dsl::*;
920 0 : self.with_measured_conn(
921 0 : DatabaseOperation::ListMetadataHealthUnhealthy,
922 0 : move |conn| -> DatabaseResult<_> {
923 0 : Ok(crate::schema::metadata_health::table
924 0 : .filter(healthy.eq(false))
925 0 : .load::<MetadataHealthPersistence>(conn)?)
926 0 : },
927 0 : )
928 0 : .await
929 0 : }
930 :
931 : /// Lists all the metadata health records that have not been updated since an `earlier` time.
932 : #[allow(dead_code)]
933 0 : pub(crate) async fn list_outdated_metadata_health_records(
934 0 : &self,
935 0 : earlier: chrono::DateTime<chrono::Utc>,
936 0 : ) -> DatabaseResult<Vec<MetadataHealthPersistence>> {
937 : use crate::schema::metadata_health::dsl::*;
938 :
939 0 : self.with_measured_conn(
940 0 : DatabaseOperation::ListMetadataHealthOutdated,
941 0 : move |conn| -> DatabaseResult<_> {
942 0 : let query = metadata_health.filter(last_scrubbed_at.lt(earlier));
943 0 : let res = query.load::<MetadataHealthPersistence>(conn)?;
944 :
945 0 : Ok(res)
946 0 : },
947 0 : )
948 0 : .await
949 0 : }
950 :
951 : /// Get the current entry from the `leader` table if one exists.
952 : /// It is an error for the table to contain more than one entry.
953 0 : pub(crate) async fn get_leader(&self) -> DatabaseResult<Option<ControllerPersistence>> {
954 0 : let mut leader: Vec<ControllerPersistence> = self
955 0 : .with_measured_conn(
956 0 : DatabaseOperation::GetLeader,
957 0 : move |conn| -> DatabaseResult<_> {
958 0 : Ok(crate::schema::controllers::table.load::<ControllerPersistence>(conn)?)
959 0 : },
960 0 : )
961 0 : .await?;
962 :
963 0 : if leader.len() > 1 {
964 0 : return Err(DatabaseError::Logical(format!(
965 0 : "More than one entry present in the leader table: {leader:?}"
966 0 : )));
967 0 : }
968 0 :
969 0 : Ok(leader.pop())
970 0 : }
971 :
972 : /// Update the new leader with compare-exchange semantics. If `prev` does not
973 : /// match the current leader entry, then the update is treated as a failure.
974 : /// When `prev` is not specified, the update is forced.
975 0 : pub(crate) async fn update_leader(
976 0 : &self,
977 0 : prev: Option<ControllerPersistence>,
978 0 : new: ControllerPersistence,
979 0 : ) -> DatabaseResult<()> {
980 : use crate::schema::controllers::dsl::*;
981 :
982 0 : let updated = self
983 0 : .with_measured_conn(
984 0 : DatabaseOperation::UpdateLeader,
985 0 : move |conn| -> DatabaseResult<usize> {
986 0 : let updated = match &prev {
987 0 : Some(prev) => diesel::update(controllers)
988 0 : .filter(address.eq(prev.address.clone()))
989 0 : .filter(started_at.eq(prev.started_at))
990 0 : .set((
991 0 : address.eq(new.address.clone()),
992 0 : started_at.eq(new.started_at),
993 0 : ))
994 0 : .execute(conn)?,
995 0 : None => diesel::insert_into(controllers)
996 0 : .values(new.clone())
997 0 : .execute(conn)?,
998 : };
999 :
1000 0 : Ok(updated)
1001 0 : },
1002 0 : )
1003 0 : .await?;
1004 :
1005 0 : if updated == 0 {
1006 0 : return Err(DatabaseError::Logical(
1007 0 : "Leader table update failed".to_string(),
1008 0 : ));
1009 0 : }
1010 0 :
1011 0 : Ok(())
1012 0 : }
1013 :
1014 0 : pub(crate) async fn safekeeper_get(
1015 0 : &self,
1016 0 : id: i64,
1017 0 : ) -> Result<SafekeeperPersistence, DatabaseError> {
1018 : use crate::schema::safekeepers::dsl::{id as id_column, safekeepers};
1019 0 : self.with_conn(move |conn| -> DatabaseResult<SafekeeperPersistence> {
1020 0 : Ok(safekeepers
1021 0 : .filter(id_column.eq(&id))
1022 0 : .select(SafekeeperPersistence::as_select())
1023 0 : .get_result(conn)?)
1024 0 : })
1025 0 : .await
1026 0 : }
1027 :
1028 0 : pub(crate) async fn safekeeper_upsert(
1029 0 : &self,
1030 0 : record: SafekeeperPersistence,
1031 0 : ) -> Result<(), DatabaseError> {
1032 : use crate::schema::safekeepers::dsl::*;
1033 :
1034 0 : self.with_conn(move |conn| -> DatabaseResult<()> {
1035 0 : let bind = record.as_insert_or_update();
1036 :
1037 0 : let inserted_updated = diesel::insert_into(safekeepers)
1038 0 : .values(&bind)
1039 0 : .on_conflict(id)
1040 0 : .do_update()
1041 0 : .set(&bind)
1042 0 : .execute(conn)?;
1043 :
1044 0 : if inserted_updated != 1 {
1045 0 : return Err(DatabaseError::Logical(format!(
1046 0 : "unexpected number of rows ({})",
1047 0 : inserted_updated
1048 0 : )));
1049 0 : }
1050 0 :
1051 0 : Ok(())
1052 0 : })
1053 0 : .await
1054 0 : }
1055 : }
1056 :
1057 : /// Parts of [`crate::tenant_shard::TenantShard`] that are stored durably
1058 : #[derive(
1059 0 : QueryableByName, Queryable, Selectable, Insertable, Serialize, Deserialize, Clone, Eq, PartialEq,
1060 : )]
1061 : #[diesel(table_name = crate::schema::tenant_shards)]
1062 : pub(crate) struct TenantShardPersistence {
1063 : #[serde(default)]
1064 : pub(crate) tenant_id: String,
1065 : #[serde(default)]
1066 : pub(crate) shard_number: i32,
1067 : #[serde(default)]
1068 : pub(crate) shard_count: i32,
1069 : #[serde(default)]
1070 : pub(crate) shard_stripe_size: i32,
1071 :
1072 : // Latest generation number: next time we attach, increment this
1073 : // and use the incremented number when attaching.
1074 : //
1075 : // Generation is only None when first onboarding a tenant, where it may
1076 : // be in PlacementPolicy::Secondary and therefore have no valid generation state.
1077 : pub(crate) generation: Option<i32>,
1078 :
1079 : // Currently attached pageserver
1080 : #[serde(rename = "pageserver")]
1081 : pub(crate) generation_pageserver: Option<i64>,
1082 :
1083 : #[serde(default)]
1084 : pub(crate) placement_policy: String,
1085 : #[serde(default)]
1086 : pub(crate) splitting: SplitState,
1087 : #[serde(default)]
1088 : pub(crate) config: String,
1089 : #[serde(default)]
1090 : pub(crate) scheduling_policy: String,
1091 :
1092 : // Hint that we should attempt to schedule this tenant shard the given
1093 : // availability zone in order to minimise the chances of cross-AZ communication
1094 : // with compute.
1095 : pub(crate) preferred_az_id: Option<String>,
1096 : }
1097 :
1098 : impl TenantShardPersistence {
1099 0 : pub(crate) fn get_shard_identity(&self) -> Result<ShardIdentity, ShardConfigError> {
1100 0 : if self.shard_count == 0 {
1101 0 : Ok(ShardIdentity::unsharded())
1102 : } else {
1103 0 : Ok(ShardIdentity::new(
1104 0 : ShardNumber(self.shard_number as u8),
1105 0 : ShardCount::new(self.shard_count as u8),
1106 0 : ShardStripeSize(self.shard_stripe_size as u32),
1107 0 : )?)
1108 : }
1109 0 : }
1110 :
1111 0 : pub(crate) fn get_tenant_shard_id(&self) -> Result<TenantShardId, hex::FromHexError> {
1112 0 : Ok(TenantShardId {
1113 0 : tenant_id: TenantId::from_str(self.tenant_id.as_str())?,
1114 0 : shard_number: ShardNumber(self.shard_number as u8),
1115 0 : shard_count: ShardCount::new(self.shard_count as u8),
1116 : })
1117 0 : }
1118 : }
1119 :
1120 : /// Parts of [`crate::node::Node`] that are stored durably
1121 0 : #[derive(Serialize, Deserialize, Queryable, Selectable, Insertable, Eq, PartialEq)]
1122 : #[diesel(table_name = crate::schema::nodes)]
1123 : pub(crate) struct NodePersistence {
1124 : pub(crate) node_id: i64,
1125 : pub(crate) scheduling_policy: String,
1126 : pub(crate) listen_http_addr: String,
1127 : pub(crate) listen_http_port: i32,
1128 : pub(crate) listen_pg_addr: String,
1129 : pub(crate) listen_pg_port: i32,
1130 : pub(crate) availability_zone_id: String,
1131 : }
1132 :
1133 : /// Tenant metadata health status that are stored durably.
1134 0 : #[derive(Queryable, Selectable, Insertable, Serialize, Deserialize, Clone, Eq, PartialEq)]
1135 : #[diesel(table_name = crate::schema::metadata_health)]
1136 : pub(crate) struct MetadataHealthPersistence {
1137 : #[serde(default)]
1138 : pub(crate) tenant_id: String,
1139 : #[serde(default)]
1140 : pub(crate) shard_number: i32,
1141 : #[serde(default)]
1142 : pub(crate) shard_count: i32,
1143 :
1144 : pub(crate) healthy: bool,
1145 : pub(crate) last_scrubbed_at: chrono::DateTime<chrono::Utc>,
1146 : }
1147 :
1148 : impl MetadataHealthPersistence {
1149 0 : pub fn new(
1150 0 : tenant_shard_id: TenantShardId,
1151 0 : healthy: bool,
1152 0 : last_scrubbed_at: chrono::DateTime<chrono::Utc>,
1153 0 : ) -> Self {
1154 0 : let tenant_id = tenant_shard_id.tenant_id.to_string();
1155 0 : let shard_number = tenant_shard_id.shard_number.0 as i32;
1156 0 : let shard_count = tenant_shard_id.shard_count.literal() as i32;
1157 0 :
1158 0 : MetadataHealthPersistence {
1159 0 : tenant_id,
1160 0 : shard_number,
1161 0 : shard_count,
1162 0 : healthy,
1163 0 : last_scrubbed_at,
1164 0 : }
1165 0 : }
1166 :
1167 : #[allow(dead_code)]
1168 0 : pub(crate) fn get_tenant_shard_id(&self) -> Result<TenantShardId, hex::FromHexError> {
1169 0 : Ok(TenantShardId {
1170 0 : tenant_id: TenantId::from_str(self.tenant_id.as_str())?,
1171 0 : shard_number: ShardNumber(self.shard_number as u8),
1172 0 : shard_count: ShardCount::new(self.shard_count as u8),
1173 : })
1174 0 : }
1175 : }
1176 :
1177 : impl From<MetadataHealthPersistence> for MetadataHealthRecord {
1178 0 : fn from(value: MetadataHealthPersistence) -> Self {
1179 0 : MetadataHealthRecord {
1180 0 : tenant_shard_id: value
1181 0 : .get_tenant_shard_id()
1182 0 : .expect("stored tenant id should be valid"),
1183 0 : healthy: value.healthy,
1184 0 : last_scrubbed_at: value.last_scrubbed_at,
1185 0 : }
1186 0 : }
1187 : }
1188 :
1189 : #[derive(
1190 0 : Serialize, Deserialize, Queryable, Selectable, Insertable, Eq, PartialEq, Debug, Clone,
1191 : )]
1192 : #[diesel(table_name = crate::schema::controllers)]
1193 : pub(crate) struct ControllerPersistence {
1194 : pub(crate) address: String,
1195 : pub(crate) started_at: chrono::DateTime<chrono::Utc>,
1196 : }
1197 :
1198 0 : #[derive(Serialize, Deserialize, Queryable, Selectable, Eq, PartialEq, Debug, Clone)]
1199 : #[diesel(table_name = crate::schema::safekeepers)]
1200 : pub(crate) struct SafekeeperPersistence {
1201 : pub(crate) id: i64,
1202 : pub(crate) region_id: String,
1203 : /// 1 is special, it means just created (not currently posted to storcon).
1204 : /// Zero or negative is not really expected.
1205 : /// Otherwise the number from `release-$(number_of_commits_on_branch)` tag.
1206 : pub(crate) version: i64,
1207 : pub(crate) host: String,
1208 : pub(crate) port: i32,
1209 : pub(crate) active: bool,
1210 : pub(crate) http_port: i32,
1211 : pub(crate) availability_zone_id: String,
1212 : }
1213 :
1214 : impl SafekeeperPersistence {
1215 0 : fn as_insert_or_update(&self) -> InsertUpdateSafekeeper<'_> {
1216 0 : InsertUpdateSafekeeper {
1217 0 : id: self.id,
1218 0 : region_id: &self.region_id,
1219 0 : version: self.version,
1220 0 : host: &self.host,
1221 0 : port: self.port,
1222 0 : active: self.active,
1223 0 : http_port: self.http_port,
1224 0 : availability_zone_id: &self.availability_zone_id,
1225 0 : }
1226 0 : }
1227 : }
1228 :
1229 0 : #[derive(Insertable, AsChangeset)]
1230 : #[diesel(table_name = crate::schema::safekeepers)]
1231 : struct InsertUpdateSafekeeper<'a> {
1232 : id: i64,
1233 : region_id: &'a str,
1234 : version: i64,
1235 : host: &'a str,
1236 : port: i32,
1237 : active: bool,
1238 : http_port: i32,
1239 : availability_zone_id: &'a str,
1240 : }
|