Line data Source code
1 : //! WAL receiver logic that ensures the pageserver gets connectected to safekeeper,
2 : //! that contains the latest WAL to stream and this connection does not go stale.
3 : //!
4 : //! To achieve that, a storage broker is used: safekepers propagate their timelines' state in it,
5 : //! the manager subscribes for changes and accumulates those to query the one with the biggest Lsn for connection.
6 : //! Current connection state is tracked too, to ensure it's not getting stale.
7 : //!
8 : //! After every connection or storage broker update fetched, the state gets updated correspondingly and rechecked for the new conneciton leader,
9 : //! then a (re)connection happens, if necessary.
10 : //! Only WAL streaming task expects to be finished, other loops (storage broker, connection management) never exit unless cancelled explicitly via the dedicated channel.
11 :
12 : use std::{collections::HashMap, num::NonZeroU64, ops::ControlFlow, sync::Arc, time::Duration};
13 :
14 : use super::{TaskStateUpdate, WalReceiverConf};
15 : use crate::context::{DownloadBehavior, RequestContext};
16 : use crate::metrics::{
17 : WALRECEIVER_ACTIVE_MANAGERS, WALRECEIVER_BROKER_UPDATES, WALRECEIVER_CANDIDATES_ADDED,
18 : WALRECEIVER_CANDIDATES_REMOVED, WALRECEIVER_SWITCHES,
19 : };
20 : use crate::task_mgr::{shutdown_token, TaskKind};
21 : use crate::tenant::{debug_assert_current_span_has_tenant_and_timeline_id, Timeline};
22 : use anyhow::Context;
23 : use chrono::{NaiveDateTime, Utc};
24 : use pageserver_api::models::TimelineState;
25 : use storage_broker::proto::subscribe_safekeeper_info_request::SubscriptionKey;
26 : use storage_broker::proto::SafekeeperTimelineInfo;
27 : use storage_broker::proto::SubscribeSafekeeperInfoRequest;
28 : use storage_broker::proto::TenantTimelineId as ProtoTenantTimelineId;
29 : use storage_broker::{BrokerClientChannel, Code, Streaming};
30 : use tokio::select;
31 : use tracing::*;
32 :
33 : use postgres_connection::PgConnectionConfig;
34 : use utils::backoff::{
35 : exponential_backoff, DEFAULT_BASE_BACKOFF_SECONDS, DEFAULT_MAX_BACKOFF_SECONDS,
36 : };
37 : use utils::postgres_client::wal_stream_connection_config;
38 : use utils::{
39 : id::{NodeId, TenantTimelineId},
40 : lsn::Lsn,
41 : };
42 :
43 : use super::{
44 : walreceiver_connection::WalConnectionStatus, walreceiver_connection::WalReceiverError,
45 : TaskEvent, TaskHandle,
46 : };
47 :
48 : /// Attempts to subscribe for timeline updates, pushed by safekeepers into the broker.
49 : /// Based on the updates, desides whether to start, keep or stop a WAL receiver task.
50 : /// If storage broker subscription is cancelled, exits.
51 1257 : pub(super) async fn connection_manager_loop_step(
52 1257 : broker_client: &mut BrokerClientChannel,
53 1257 : connection_manager_state: &mut ConnectionManagerState,
54 1257 : ctx: &RequestContext,
55 1257 : manager_status: &std::sync::RwLock<Option<ConnectionManagerStatus>>,
56 1257 : ) -> ControlFlow<(), ()> {
57 1257 : match connection_manager_state
58 1257 : .timeline
59 1257 : .wait_to_become_active(ctx)
60 23 : .await
61 : {
62 1256 : Ok(()) => {}
63 1 : Err(new_state) => {
64 0 : debug!(
65 0 : ?new_state,
66 0 : "state changed, stopping wal connection manager loop"
67 0 : );
68 1 : return ControlFlow::Break(());
69 : }
70 : }
71 :
72 1256 : WALRECEIVER_ACTIVE_MANAGERS.inc();
73 559 : scopeguard::defer! {
74 559 : WALRECEIVER_ACTIVE_MANAGERS.dec();
75 559 : }
76 :
77 1256 : let id = TenantTimelineId {
78 1256 : tenant_id: connection_manager_state.timeline.tenant_shard_id.tenant_id,
79 1256 : timeline_id: connection_manager_state.timeline.timeline_id,
80 1256 : };
81 1256 :
82 1256 : let mut timeline_state_updates = connection_manager_state
83 1256 : .timeline
84 1256 : .subscribe_for_state_updates();
85 :
86 : // Subscribe to the broker updates. Stream shares underlying TCP connection
87 : // with other streams on this client (other connection managers). When
88 : // object goes out of scope, stream finishes in drop() automatically.
89 2500 : let mut broker_subscription = subscribe_for_timeline_updates(broker_client, id).await;
90 0 : debug!("Subscribed for broker timeline updates");
91 :
92 : loop {
93 736370 : let time_until_next_retry = connection_manager_state.time_until_next_retry();
94 736370 :
95 736370 : // These things are happening concurrently:
96 736370 : //
97 736370 : // - keep receiving WAL on the current connection
98 736370 : // - if the shared state says we need to change connection, disconnect and return
99 736370 : // - this runs in a separate task and we receive updates via a watch channel
100 736370 : // - change connection if the rules decide so, or if the current connection dies
101 736370 : // - receive updates from broker
102 736370 : // - this might change the current desired connection
103 736370 : // - timeline state changes to something that does not allow walreceiver to run concurrently
104 1450465 : select! {
105 736218 : Some(wal_connection_update) = async {
106 736218 : match connection_manager_state.wal_connection.as_mut() {
107 734073 : Some(wal_connection) => Some(wal_connection.connection_task.next_task_event().await),
108 2145 : None => None,
109 : }
110 728400 : } => {
111 : let wal_connection = connection_manager_state.wal_connection.as_mut()
112 : .expect("Should have a connection, as checked by the corresponding select! guard");
113 : match wal_connection_update {
114 : TaskEvent::Update(TaskStateUpdate::Started) => {},
115 : TaskEvent::Update(TaskStateUpdate::Progress(new_status)) => {
116 : if new_status.has_processed_wal {
117 : // We have advanced last_record_lsn by processing the WAL received
118 : // from this safekeeper. This is good enough to clean unsuccessful
119 : // retries history and allow reconnecting to this safekeeper without
120 : // sleeping for a long time.
121 : connection_manager_state.wal_connection_retries.remove(&wal_connection.sk_id);
122 : }
123 : wal_connection.status = new_status;
124 : }
125 : TaskEvent::End(walreceiver_task_result) => {
126 : match walreceiver_task_result {
127 0 : Ok(()) => debug!("WAL receiving task finished"),
128 13 : Err(e) => error!("wal receiver task finished with an error: {e:?}"),
129 : }
130 : connection_manager_state.drop_old_connection(false).await;
131 : },
132 : }
133 : },
134 :
135 : // Got a new update from the broker
136 8263 : broker_update = broker_subscription.message() => {
137 : match broker_update {
138 : Ok(Some(broker_update)) => connection_manager_state.register_timeline_update(broker_update),
139 : Err(status) => {
140 : match status.code() {
141 : Code::Unknown if status.message().contains("stream closed because of a broken pipe") || status.message().contains("connection reset") => {
142 : // tonic's error handling doesn't provide a clear code for disconnections: we get
143 : // "h2 protocol error: error reading a body from connection: stream closed because of a broken pipe"
144 0 : info!("broker disconnected: {status}");
145 : },
146 : _ => {
147 0 : warn!("broker subscription failed: {status}");
148 : }
149 : }
150 : return ControlFlow::Continue(());
151 : }
152 : Ok(None) => {
153 0 : error!("broker subscription stream ended"); // can't happen
154 : return ControlFlow::Continue(());
155 : }
156 : }
157 : },
158 :
159 726333 : new_event = async {
160 726333 : loop {
161 726333 : if connection_manager_state.timeline.current_state() == TimelineState::Loading {
162 0 : warn!("wal connection manager should only be launched after timeline has become active");
163 726333 : }
164 726333 : match timeline_state_updates.changed().await {
165 : Ok(()) => {
166 504 : let new_state = connection_manager_state.timeline.current_state();
167 504 : match new_state {
168 : // we're already active as walreceiver, no need to reactivate
169 0 : TimelineState::Active => continue,
170 : TimelineState::Broken { .. } | TimelineState::Stopping => {
171 504 : debug!("timeline entered terminal state {new_state:?}, stopping wal connection manager loop");
172 504 : return ControlFlow::Break(());
173 : }
174 : TimelineState::Loading => {
175 0 : warn!("timeline transitioned back to Loading state, that should not happen");
176 0 : return ControlFlow::Continue(());
177 : }
178 : }
179 : }
180 0 : Err(_sender_dropped_error) => return ControlFlow::Break(()),
181 : }
182 : }
183 504 : } => match new_event {
184 : ControlFlow::Continue(()) => {
185 : return ControlFlow::Continue(());
186 : }
187 : ControlFlow::Break(()) => {
188 0 : debug!("Timeline is no longer active, stopping wal connection manager loop");
189 : return ControlFlow::Break(());
190 : }
191 : },
192 :
193 731320 : Some(()) = async {
194 731320 : match time_until_next_retry {
195 1393 : Some(sleep_time) => {
196 1393 : tokio::time::sleep(sleep_time).await;
197 614 : Some(())
198 : },
199 : None => {
200 729927 : debug!("No candidates to retry, waiting indefinitely for the broker events");
201 729927 : None
202 : }
203 : }
204 730541 : } => debug!("Waking up for the next retry after waiting for {time_until_next_retry:?}"),
205 : }
206 :
207 735131 : if let Some(new_candidate) = connection_manager_state.next_connection_candidate() {
208 1703 : info!("Switching to new connection candidate: {new_candidate:?}");
209 1703 : connection_manager_state
210 1703 : .change_connection(new_candidate, ctx)
211 14 : .await
212 733428 : }
213 735131 : *manager_status.write().unwrap() = Some(connection_manager_state.manager_status());
214 : }
215 505 : }
216 :
217 : /// Endlessly try to subscribe for broker updates for a given timeline.
218 1256 : async fn subscribe_for_timeline_updates(
219 1256 : broker_client: &mut BrokerClientChannel,
220 1256 : id: TenantTimelineId,
221 1256 : ) -> Streaming<SafekeeperTimelineInfo> {
222 1256 : let mut attempt = 0;
223 1256 : let cancel = shutdown_token();
224 :
225 : loop {
226 1256 : exponential_backoff(
227 1256 : attempt,
228 1256 : DEFAULT_BASE_BACKOFF_SECONDS,
229 1256 : DEFAULT_MAX_BACKOFF_SECONDS,
230 1256 : &cancel,
231 1256 : )
232 0 : .await;
233 1256 : attempt += 1;
234 1256 :
235 1256 : // subscribe to the specific timeline
236 1256 : let key = SubscriptionKey::TenantTimelineId(ProtoTenantTimelineId {
237 1256 : tenant_id: id.tenant_id.as_ref().to_owned(),
238 1256 : timeline_id: id.timeline_id.as_ref().to_owned(),
239 1256 : });
240 1256 : let request = SubscribeSafekeeperInfoRequest {
241 1256 : subscription_key: Some(key),
242 1256 : };
243 1256 :
244 2500 : match broker_client.subscribe_safekeeper_info(request).await {
245 1239 : Ok(resp) => {
246 1239 : return resp.into_inner();
247 : }
248 0 : Err(e) => {
249 : // Safekeeper nodes can stop pushing timeline updates to the broker, when no new writes happen and
250 : // entire WAL is streamed. Keep this noticeable with logging, but do not warn/error.
251 0 : info!("Attempt #{attempt}, failed to subscribe for timeline {id} updates in broker: {e:#}");
252 0 : continue;
253 : }
254 : }
255 : }
256 1239 : }
257 :
258 : const WALCONNECTION_RETRY_MIN_BACKOFF_SECONDS: f64 = 0.1;
259 : const WALCONNECTION_RETRY_MAX_BACKOFF_SECONDS: f64 = 15.0;
260 : const WALCONNECTION_RETRY_BACKOFF_MULTIPLIER: f64 = 1.5;
261 :
262 : /// All data that's needed to run endless broker loop and keep the WAL streaming connection alive, if possible.
263 : pub(super) struct ConnectionManagerState {
264 : id: TenantTimelineId,
265 : /// Use pageserver data about the timeline to filter out some of the safekeepers.
266 : timeline: Arc<Timeline>,
267 : conf: WalReceiverConf,
268 : /// Current connection to safekeeper for WAL streaming.
269 : wal_connection: Option<WalConnection>,
270 : /// Info about retries and unsuccessful attempts to connect to safekeepers.
271 : wal_connection_retries: HashMap<NodeId, RetryInfo>,
272 : /// Data about all timelines, available for connection, fetched from storage broker, grouped by their corresponding safekeeper node id.
273 : wal_stream_candidates: HashMap<NodeId, BrokerSkTimeline>,
274 : }
275 :
276 : /// An information about connection manager's current connection and connection candidates.
277 1865 : #[derive(Debug, Clone)]
278 : pub struct ConnectionManagerStatus {
279 : existing_connection: Option<WalConnectionStatus>,
280 : wal_stream_candidates: HashMap<NodeId, BrokerSkTimeline>,
281 : }
282 :
283 : impl ConnectionManagerStatus {
284 : /// Generates a string, describing current connection status in a form, suitable for logging.
285 1865 : pub fn to_human_readable_string(&self) -> String {
286 1865 : let mut resulting_string = String::new();
287 1865 : match &self.existing_connection {
288 1819 : Some(connection) => {
289 1819 : if connection.has_processed_wal {
290 1783 : resulting_string.push_str(&format!(
291 1783 : " (update {}): streaming WAL from node {}, ",
292 1783 : connection.latest_wal_update.format("%Y-%m-%d %H:%M:%S"),
293 1783 : connection.node,
294 1783 : ));
295 1783 :
296 1783 : match (connection.streaming_lsn, connection.commit_lsn) {
297 0 : (None, None) => resulting_string.push_str("no streaming data"),
298 0 : (None, Some(commit_lsn)) => {
299 0 : resulting_string.push_str(&format!("commit Lsn: {commit_lsn}"))
300 : }
301 0 : (Some(streaming_lsn), None) => {
302 0 : resulting_string.push_str(&format!("streaming Lsn: {streaming_lsn}"))
303 : }
304 1783 : (Some(streaming_lsn), Some(commit_lsn)) => resulting_string.push_str(
305 1783 : &format!("commit|streaming Lsn: {commit_lsn}|{streaming_lsn}"),
306 1783 : ),
307 : }
308 36 : } else if connection.is_connected {
309 36 : resulting_string.push_str(&format!(
310 36 : " (update {}): connecting to node {}",
311 36 : connection
312 36 : .latest_connection_update
313 36 : .format("%Y-%m-%d %H:%M:%S"),
314 36 : connection.node,
315 36 : ));
316 36 : } else {
317 0 : resulting_string.push_str(&format!(
318 0 : " (update {}): initializing node {} connection",
319 0 : connection
320 0 : .latest_connection_update
321 0 : .format("%Y-%m-%d %H:%M:%S"),
322 0 : connection.node,
323 0 : ));
324 0 : }
325 : }
326 46 : None => resulting_string.push_str(": disconnected"),
327 : }
328 :
329 1865 : resulting_string.push_str(", safekeeper candidates (id|update_time|commit_lsn): [");
330 1865 : let mut candidates = self.wal_stream_candidates.iter().peekable();
331 3875 : while let Some((node_id, candidate_info)) = candidates.next() {
332 2010 : resulting_string.push_str(&format!(
333 2010 : "({}|{}|{})",
334 2010 : node_id,
335 2010 : candidate_info.latest_update.format("%H:%M:%S"),
336 2010 : Lsn(candidate_info.timeline.commit_lsn)
337 2010 : ));
338 2010 : if candidates.peek().is_some() {
339 146 : resulting_string.push_str(", ");
340 1864 : }
341 : }
342 1865 : resulting_string.push(']');
343 1865 :
344 1865 : resulting_string
345 1865 : }
346 : }
347 :
348 : /// Current connection data.
349 0 : #[derive(Debug)]
350 : struct WalConnection {
351 : /// Time when the connection was initiated.
352 : started_at: NaiveDateTime,
353 : /// Current safekeeper pageserver is connected to for WAL streaming.
354 : sk_id: NodeId,
355 : /// Availability zone of the safekeeper.
356 : availability_zone: Option<String>,
357 : /// Status of the connection.
358 : status: WalConnectionStatus,
359 : /// WAL streaming task handle.
360 : connection_task: TaskHandle<WalConnectionStatus>,
361 : /// Have we discovered that other safekeeper has more recent WAL than we do?
362 : discovered_new_wal: Option<NewCommittedWAL>,
363 : }
364 :
365 : /// Notion of a new committed WAL, which exists on other safekeeper.
366 0 : #[derive(Debug, Clone, Copy)]
367 : struct NewCommittedWAL {
368 : /// LSN of the new committed WAL.
369 : lsn: Lsn,
370 : /// When we discovered that the new committed WAL exists on other safekeeper.
371 : discovered_at: NaiveDateTime,
372 : }
373 :
374 0 : #[derive(Debug, Clone, Copy)]
375 : struct RetryInfo {
376 : next_retry_at: Option<NaiveDateTime>,
377 : retry_duration_seconds: f64,
378 : }
379 :
380 : /// Data about the timeline to connect to, received from the broker.
381 1163869 : #[derive(Debug, Clone)]
382 : struct BrokerSkTimeline {
383 : timeline: SafekeeperTimelineInfo,
384 : /// Time at which the data was fetched from the broker last time, to track the stale data.
385 : latest_update: NaiveDateTime,
386 : }
387 :
388 : impl ConnectionManagerState {
389 1257 : pub(super) fn new(timeline: Arc<Timeline>, conf: WalReceiverConf) -> Self {
390 1257 : let id = TenantTimelineId {
391 1257 : tenant_id: timeline.tenant_shard_id.tenant_id,
392 1257 : timeline_id: timeline.timeline_id,
393 1257 : };
394 1257 : Self {
395 1257 : id,
396 1257 : timeline,
397 1257 : conf,
398 1257 : wal_connection: None,
399 1257 : wal_stream_candidates: HashMap::new(),
400 1257 : wal_connection_retries: HashMap::new(),
401 1257 : }
402 1257 : }
403 :
404 : /// Shuts down the current connection (if any) and immediately starts another one with the given connection string.
405 1703 : async fn change_connection(&mut self, new_sk: NewWalConnectionCandidate, ctx: &RequestContext) {
406 1703 : WALRECEIVER_SWITCHES
407 1703 : .with_label_values(&[new_sk.reason.name()])
408 1703 : .inc();
409 1703 :
410 1703 : self.drop_old_connection(true).await;
411 :
412 1703 : let node_id = new_sk.safekeeper_id;
413 1703 : let connect_timeout = self.conf.wal_connect_timeout;
414 1703 : let ingest_batch_size = self.conf.ingest_batch_size;
415 1703 : let timeline = Arc::clone(&self.timeline);
416 1703 : let ctx = ctx.detached_child(
417 1703 : TaskKind::WalReceiverConnectionHandler,
418 1703 : DownloadBehavior::Download,
419 1703 : );
420 :
421 1703 : let span = info_span!("connection", %node_id);
422 1703 : let connection_handle = TaskHandle::spawn(move |events_sender, cancellation| {
423 1703 : async move {
424 1703 : debug_assert_current_span_has_tenant_and_timeline_id();
425 :
426 1703 : let res = super::walreceiver_connection::handle_walreceiver_connection(
427 1703 : timeline,
428 1703 : new_sk.wal_source_connconf,
429 1703 : events_sender,
430 1703 : cancellation.clone(),
431 1703 : connect_timeout,
432 1703 : ctx,
433 1703 : node_id,
434 1703 : ingest_batch_size,
435 1703 : )
436 836156 : .await;
437 :
438 1659 : match res {
439 219 : Ok(()) => Ok(()),
440 1440 : Err(e) => {
441 1440 : match e {
442 27 : WalReceiverError::SuccessfulCompletion(msg) => {
443 27 : info!("walreceiver connection handling ended with success: {msg}");
444 27 : Ok(())
445 : }
446 1394 : WalReceiverError::ExpectedSafekeeperError(e) => {
447 1394 : info!("walreceiver connection handling ended: {e}");
448 1394 : Ok(())
449 : }
450 19 : WalReceiverError::Other(e) => {
451 19 : // give out an error to have task_mgr give it a really verbose logging
452 19 : if cancellation.is_cancelled() {
453 : // Ideally we would learn about this via some path other than Other, but
454 : // that requires refactoring all the intermediate layers of ingest code
455 : // that only emit anyhow::Error
456 4 : Ok(())
457 : } else {
458 15 : Err(e).context("walreceiver connection handling failure")
459 : }
460 : }
461 : }
462 : }
463 : }
464 1659 : }
465 1703 : .instrument(span)
466 1703 : });
467 1703 :
468 1703 : let now = Utc::now().naive_utc();
469 1703 : self.wal_connection = Some(WalConnection {
470 1703 : started_at: now,
471 1703 : sk_id: new_sk.safekeeper_id,
472 1703 : availability_zone: new_sk.availability_zone,
473 1703 : status: WalConnectionStatus {
474 1703 : is_connected: false,
475 1703 : has_processed_wal: false,
476 1703 : latest_connection_update: now,
477 1703 : latest_wal_update: now,
478 1703 : streaming_lsn: None,
479 1703 : commit_lsn: None,
480 1703 : node: node_id,
481 1703 : },
482 1703 : connection_task: connection_handle,
483 1703 : discovered_new_wal: None,
484 1703 : });
485 1703 : }
486 :
487 : /// Drops the current connection (if any) and updates retry timeout for the next
488 : /// connection attempt to the same safekeeper.
489 3076 : async fn drop_old_connection(&mut self, needs_shutdown: bool) {
490 3076 : let wal_connection = match self.wal_connection.take() {
491 1387 : Some(wal_connection) => wal_connection,
492 1689 : None => return,
493 : };
494 :
495 1387 : if needs_shutdown {
496 14 : wal_connection.connection_task.shutdown().await;
497 1373 : }
498 :
499 1387 : let retry = self
500 1387 : .wal_connection_retries
501 1387 : .entry(wal_connection.sk_id)
502 1387 : .or_insert(RetryInfo {
503 1387 : next_retry_at: None,
504 1387 : retry_duration_seconds: WALCONNECTION_RETRY_MIN_BACKOFF_SECONDS,
505 1387 : });
506 1387 :
507 1387 : let now = Utc::now().naive_utc();
508 1387 :
509 1387 : // Schedule the next retry attempt. We want to have exponential backoff for connection attempts,
510 1387 : // and we add backoff to the time when we started the connection attempt. If the connection
511 1387 : // was active for a long time, then next_retry_at will be in the past.
512 1387 : retry.next_retry_at =
513 1387 : wal_connection
514 1387 : .started_at
515 1387 : .checked_add_signed(chrono::Duration::milliseconds(
516 1387 : (retry.retry_duration_seconds * 1000.0) as i64,
517 1387 : ));
518 :
519 1387 : if let Some(next) = &retry.next_retry_at {
520 1387 : if next > &now {
521 994 : info!(
522 994 : "Next connection retry to {:?} is at {}",
523 994 : wal_connection.sk_id, next
524 994 : );
525 393 : }
526 0 : }
527 :
528 1387 : let next_retry_duration =
529 1387 : retry.retry_duration_seconds * WALCONNECTION_RETRY_BACKOFF_MULTIPLIER;
530 1387 : // Clamp the next retry duration to the maximum allowed.
531 1387 : let next_retry_duration = next_retry_duration.min(WALCONNECTION_RETRY_MAX_BACKOFF_SECONDS);
532 1387 : // Clamp the next retry duration to the minimum allowed.
533 1387 : let next_retry_duration = next_retry_duration.max(WALCONNECTION_RETRY_MIN_BACKOFF_SECONDS);
534 1387 :
535 1387 : retry.retry_duration_seconds = next_retry_duration;
536 3076 : }
537 :
538 : /// Returns time needed to wait to have a new candidate for WAL streaming.
539 736370 : fn time_until_next_retry(&self) -> Option<Duration> {
540 736370 : let now = Utc::now().naive_utc();
541 :
542 736370 : let next_retry_at = self
543 736370 : .wal_connection_retries
544 736370 : .values()
545 736370 : .filter_map(|retry| retry.next_retry_at)
546 736370 : .filter(|next_retry_at| next_retry_at > &now)
547 736370 : .min()?;
548 :
549 1396 : (next_retry_at - now).to_std().ok()
550 736370 : }
551 :
552 : /// Adds another broker timeline into the state, if its more recent than the one already added there for the same key.
553 8263 : fn register_timeline_update(&mut self, timeline_update: SafekeeperTimelineInfo) {
554 8263 : WALRECEIVER_BROKER_UPDATES.inc();
555 8263 :
556 8263 : let new_safekeeper_id = NodeId(timeline_update.safekeeper_id);
557 8263 : let old_entry = self.wal_stream_candidates.insert(
558 8263 : new_safekeeper_id,
559 8263 : BrokerSkTimeline {
560 8263 : timeline: timeline_update,
561 8263 : latest_update: Utc::now().naive_utc(),
562 8263 : },
563 8263 : );
564 8263 :
565 8263 : if old_entry.is_none() {
566 687 : info!("New SK node was added: {new_safekeeper_id}");
567 687 : WALRECEIVER_CANDIDATES_ADDED.inc();
568 7576 : }
569 8263 : }
570 :
571 : /// Cleans up stale broker records and checks the rest for the new connection candidate.
572 : /// Returns a new candidate, if the current state is absent or somewhat lagging, `None` otherwise.
573 : /// The current rules for approving new candidates:
574 : /// * pick a candidate different from the connected safekeeper with biggest `commit_lsn` and lowest failed connection attemps
575 : /// * if there's no such entry, no new candidate found, abort
576 : /// * otherwise check if the candidate is much better than the current one
577 : ///
578 : /// To understand exact rules for determining if the candidate is better than the current one, refer to this function's implementation.
579 : /// General rules are following:
580 : /// * if connected safekeeper is not present, pick the candidate
581 : /// * if we haven't received any updates for some time, pick the candidate
582 : /// * if the candidate commit_lsn is much higher than the current one, pick the candidate
583 : /// * if the candidate commit_lsn is same, but candidate is located in the same AZ as the pageserver, pick the candidate
584 : /// * if connected safekeeper stopped sending us new WAL which is available on other safekeeper, pick the candidate
585 : ///
586 : /// This way we ensure to keep up with the most up-to-date safekeeper and don't try to jump from one safekeeper to another too frequently.
587 : /// Both thresholds are configured per tenant.
588 735149 : fn next_connection_candidate(&mut self) -> Option<NewWalConnectionCandidate> {
589 735149 : self.cleanup_old_candidates();
590 735149 :
591 735149 : match &self.wal_connection {
592 732543 : Some(existing_wal_connection) => {
593 732543 : let connected_sk_node = existing_wal_connection.sk_id;
594 :
595 235487 : let (new_sk_id, new_safekeeper_broker_data, new_wal_source_connconf) =
596 732543 : self.select_connection_candidate(Some(connected_sk_node))?;
597 235487 : let new_availability_zone = new_safekeeper_broker_data.availability_zone.clone();
598 235487 :
599 235487 : let now = Utc::now().naive_utc();
600 235487 : if let Ok(latest_interaciton) =
601 235487 : (now - existing_wal_connection.status.latest_connection_update).to_std()
602 : {
603 : // Drop connection if we haven't received keepalive message for a while.
604 235487 : if latest_interaciton > self.conf.wal_connect_timeout {
605 2 : return Some(NewWalConnectionCandidate {
606 2 : safekeeper_id: new_sk_id,
607 2 : wal_source_connconf: new_wal_source_connconf,
608 2 : availability_zone: new_availability_zone,
609 2 : reason: ReconnectReason::NoKeepAlives {
610 2 : last_keep_alive: Some(
611 2 : existing_wal_connection.status.latest_connection_update,
612 2 : ),
613 2 : check_time: now,
614 2 : threshold: self.conf.wal_connect_timeout,
615 2 : },
616 2 : });
617 235485 : }
618 0 : }
619 :
620 235485 : if !existing_wal_connection.status.is_connected {
621 : // We haven't connected yet and we shouldn't switch until connection timeout (condition above).
622 203 : return None;
623 235282 : }
624 :
625 235282 : if let Some(current_commit_lsn) = existing_wal_connection.status.commit_lsn {
626 235197 : let new_commit_lsn = Lsn(new_safekeeper_broker_data.commit_lsn);
627 235197 : // Check if the new candidate has much more WAL than the current one.
628 235197 : match new_commit_lsn.0.checked_sub(current_commit_lsn.0) {
629 9142 : Some(new_sk_lsn_advantage) => {
630 9142 : if new_sk_lsn_advantage >= self.conf.max_lsn_wal_lag.get() {
631 16 : return Some(NewWalConnectionCandidate {
632 16 : safekeeper_id: new_sk_id,
633 16 : wal_source_connconf: new_wal_source_connconf,
634 16 : availability_zone: new_availability_zone,
635 16 : reason: ReconnectReason::LaggingWal {
636 16 : current_commit_lsn,
637 16 : new_commit_lsn,
638 16 : threshold: self.conf.max_lsn_wal_lag,
639 16 : },
640 16 : });
641 9126 : }
642 9126 : // If we have a candidate with the same commit_lsn as the current one, which is in the same AZ as pageserver,
643 9126 : // and the current one is not, switch to the new one.
644 9126 : if self.conf.availability_zone.is_some()
645 176 : && existing_wal_connection.availability_zone
646 176 : != self.conf.availability_zone
647 176 : && self.conf.availability_zone == new_availability_zone
648 : {
649 2 : return Some(NewWalConnectionCandidate {
650 2 : safekeeper_id: new_sk_id,
651 2 : availability_zone: new_availability_zone,
652 2 : wal_source_connconf: new_wal_source_connconf,
653 2 : reason: ReconnectReason::SwitchAvailabilityZone,
654 2 : });
655 9124 : }
656 : }
657 226055 : None => debug!(
658 0 : "Best SK candidate has its commit_lsn behind connected SK's commit_lsn"
659 0 : ),
660 : }
661 85 : }
662 :
663 235264 : let current_lsn = match existing_wal_connection.status.streaming_lsn {
664 235021 : Some(lsn) => lsn,
665 243 : None => self.timeline.get_last_record_lsn(),
666 : };
667 235264 : let current_commit_lsn = existing_wal_connection
668 235264 : .status
669 235264 : .commit_lsn
670 235264 : .unwrap_or(current_lsn);
671 235264 : let candidate_commit_lsn = Lsn(new_safekeeper_broker_data.commit_lsn);
672 235264 :
673 235264 : // Keep discovered_new_wal only if connected safekeeper has not caught up yet.
674 235264 : let mut discovered_new_wal = existing_wal_connection
675 235264 : .discovered_new_wal
676 235264 : .filter(|new_wal| new_wal.lsn > current_commit_lsn);
677 235264 :
678 235264 : if discovered_new_wal.is_none() {
679 : // Check if the new candidate has more WAL than the current one.
680 : // If the new candidate has more WAL than the current one, we consider switching to the new candidate.
681 231002 : discovered_new_wal = if candidate_commit_lsn > current_commit_lsn {
682 138 : trace!(
683 0 : "New candidate has commit_lsn {}, higher than current_commit_lsn {}",
684 0 : candidate_commit_lsn,
685 0 : current_commit_lsn
686 0 : );
687 138 : Some(NewCommittedWAL {
688 138 : lsn: candidate_commit_lsn,
689 138 : discovered_at: Utc::now().naive_utc(),
690 138 : })
691 : } else {
692 230864 : None
693 : };
694 4262 : }
695 :
696 235264 : let waiting_for_new_lsn_since = if current_lsn < current_commit_lsn {
697 : // Connected safekeeper has more WAL, but we haven't received updates for some time.
698 8922 : trace!(
699 0 : "Connected safekeeper has more WAL, but we haven't received updates for {:?}. current_lsn: {}, current_commit_lsn: {}",
700 0 : (now - existing_wal_connection.status.latest_wal_update).to_std(),
701 0 : current_lsn,
702 0 : current_commit_lsn
703 0 : );
704 8922 : Some(existing_wal_connection.status.latest_wal_update)
705 : } else {
706 226342 : discovered_new_wal.as_ref().map(|new_wal| {
707 1008 : // We know that new WAL is available on other safekeeper, but connected safekeeper don't have it.
708 1008 : new_wal
709 1008 : .discovered_at
710 1008 : .max(existing_wal_connection.status.latest_wal_update)
711 226342 : })
712 : };
713 :
714 : // If we haven't received any WAL updates for a while and candidate has more WAL, switch to it.
715 235264 : if let Some(waiting_for_new_lsn_since) = waiting_for_new_lsn_since {
716 9930 : if let Ok(waiting_for_new_wal) = (now - waiting_for_new_lsn_since).to_std() {
717 9871 : if candidate_commit_lsn > current_commit_lsn
718 4339 : && waiting_for_new_wal > self.conf.lagging_wal_timeout
719 : {
720 2 : return Some(NewWalConnectionCandidate {
721 2 : safekeeper_id: new_sk_id,
722 2 : wal_source_connconf: new_wal_source_connconf,
723 2 : availability_zone: new_availability_zone,
724 2 : reason: ReconnectReason::NoWalTimeout {
725 2 : current_lsn,
726 2 : current_commit_lsn,
727 2 : candidate_commit_lsn,
728 2 : last_wal_interaction: Some(
729 2 : existing_wal_connection.status.latest_wal_update,
730 2 : ),
731 2 : check_time: now,
732 2 : threshold: self.conf.lagging_wal_timeout,
733 2 : },
734 2 : });
735 9869 : }
736 59 : }
737 225334 : }
738 :
739 235262 : self.wal_connection.as_mut().unwrap().discovered_new_wal = discovered_new_wal;
740 : }
741 : None => {
742 1695 : let (new_sk_id, new_safekeeper_broker_data, new_wal_source_connconf) =
743 2606 : self.select_connection_candidate(None)?;
744 1695 : return Some(NewWalConnectionCandidate {
745 1695 : safekeeper_id: new_sk_id,
746 1695 : availability_zone: new_safekeeper_broker_data.availability_zone.clone(),
747 1695 : wal_source_connconf: new_wal_source_connconf,
748 1695 : reason: ReconnectReason::NoExistingConnection,
749 1695 : });
750 : }
751 : }
752 :
753 235262 : None
754 735149 : }
755 :
756 : /// Selects the best possible candidate, based on the data collected from the broker updates about the safekeepers.
757 : /// Optionally, omits the given node, to support gracefully switching from a healthy safekeeper to another.
758 : ///
759 : /// The candidate that is chosen:
760 : /// * has no pending retry cooldown
761 : /// * has greatest commit_lsn among the ones that are left
762 735149 : fn select_connection_candidate(
763 735149 : &self,
764 735149 : node_to_omit: Option<NodeId>,
765 735149 : ) -> Option<(NodeId, &SafekeeperTimelineInfo, PgConnectionConfig)> {
766 735149 : self.applicable_connection_candidates()
767 1160056 : .filter(|&(sk_id, _, _)| Some(sk_id) != node_to_omit)
768 735149 : .max_by_key(|(_, info, _)| info.commit_lsn)
769 735149 : }
770 :
771 : /// Returns a list of safekeepers that have valid info and ready for connection.
772 : /// Some safekeepers are filtered by the retry cooldown.
773 735149 : fn applicable_connection_candidates(
774 735149 : &self,
775 735149 : ) -> impl Iterator<Item = (NodeId, &SafekeeperTimelineInfo, PgConnectionConfig)> {
776 735149 : let now = Utc::now().naive_utc();
777 735149 :
778 735149 : self.wal_stream_candidates
779 735149 : .iter()
780 1161895 : .filter(|(_, info)| Lsn(info.timeline.commit_lsn) != Lsn::INVALID)
781 1161891 : .filter(move |(sk_id, _)| {
782 1161891 : let next_retry_at = self
783 1161891 : .wal_connection_retries
784 1161891 : .get(sk_id)
785 1161891 : .and_then(|retry_info| {
786 206682 : retry_info.next_retry_at
787 1161891 : });
788 1161891 :
789 1161891 : next_retry_at.is_none() || next_retry_at.unwrap() <= now
790 1161891 : }).filter_map(|(sk_id, broker_info)| {
791 1160060 : let info = &broker_info.timeline;
792 1160060 : if info.safekeeper_connstr.is_empty() {
793 4 : return None; // no connection string, ignore sk
794 1160056 : }
795 1160056 : match wal_stream_connection_config(
796 1160056 : self.id,
797 1160056 : info.safekeeper_connstr.as_ref(),
798 1160056 : match &self.conf.auth_token {
799 1158838 : None => None,
800 1218 : Some(x) => Some(x),
801 : },
802 1160056 : self.conf.availability_zone.as_deref(),
803 : ) {
804 1160056 : Ok(connstr) => Some((*sk_id, info, connstr)),
805 0 : Err(e) => {
806 0 : error!("Failed to create wal receiver connection string from broker data of safekeeper node {}: {e:#}", sk_id);
807 0 : None
808 : }
809 : }
810 1160060 : })
811 735149 : }
812 :
813 : /// Remove candidates which haven't sent broker updates for a while.
814 735149 : fn cleanup_old_candidates(&mut self) {
815 735149 : let mut node_ids_to_remove = Vec::with_capacity(self.wal_stream_candidates.len());
816 735149 : let lagging_wal_timeout = self.conf.lagging_wal_timeout;
817 735149 :
818 1161906 : self.wal_stream_candidates.retain(|node_id, broker_info| {
819 1161906 : if let Ok(time_since_latest_broker_update) =
820 1161906 : (Utc::now().naive_utc() - broker_info.latest_update).to_std()
821 : {
822 1161906 : let should_retain = time_since_latest_broker_update < lagging_wal_timeout;
823 1161906 : if !should_retain {
824 11 : node_ids_to_remove.push(*node_id);
825 1161895 : }
826 1161906 : should_retain
827 : } else {
828 0 : true
829 : }
830 1161906 : });
831 735149 :
832 735149 : if !node_ids_to_remove.is_empty() {
833 22 : for node_id in node_ids_to_remove {
834 11 : info!("Safekeeper node {node_id} did not send events for over {lagging_wal_timeout:?}, not retrying the connections");
835 11 : self.wal_connection_retries.remove(&node_id);
836 11 : WALRECEIVER_CANDIDATES_REMOVED.inc();
837 : }
838 735138 : }
839 735149 : }
840 :
841 560 : pub(super) async fn shutdown(mut self) {
842 560 : if let Some(wal_connection) = self.wal_connection.take() {
843 306 : wal_connection.connection_task.shutdown().await;
844 289 : }
845 560 : }
846 :
847 735131 : fn manager_status(&self) -> ConnectionManagerStatus {
848 735131 : ConnectionManagerStatus {
849 735131 : existing_connection: self.wal_connection.as_ref().map(|conn| conn.status),
850 735131 : wal_stream_candidates: self.wal_stream_candidates.clone(),
851 735131 : }
852 735131 : }
853 : }
854 :
855 1703 : #[derive(Debug)]
856 : struct NewWalConnectionCandidate {
857 : safekeeper_id: NodeId,
858 : wal_source_connconf: PgConnectionConfig,
859 : availability_zone: Option<String>,
860 : reason: ReconnectReason,
861 : }
862 :
863 : /// Stores the reason why WAL connection was switched, for furter debugging purposes.
864 1703 : #[derive(Debug, PartialEq, Eq)]
865 : enum ReconnectReason {
866 : NoExistingConnection,
867 : LaggingWal {
868 : current_commit_lsn: Lsn,
869 : new_commit_lsn: Lsn,
870 : threshold: NonZeroU64,
871 : },
872 : SwitchAvailabilityZone,
873 : NoWalTimeout {
874 : current_lsn: Lsn,
875 : current_commit_lsn: Lsn,
876 : candidate_commit_lsn: Lsn,
877 : last_wal_interaction: Option<NaiveDateTime>,
878 : check_time: NaiveDateTime,
879 : threshold: Duration,
880 : },
881 : NoKeepAlives {
882 : last_keep_alive: Option<NaiveDateTime>,
883 : check_time: NaiveDateTime,
884 : threshold: Duration,
885 : },
886 : }
887 :
888 : impl ReconnectReason {
889 1703 : fn name(&self) -> &str {
890 1703 : match self {
891 1689 : ReconnectReason::NoExistingConnection => "NoExistingConnection",
892 14 : ReconnectReason::LaggingWal { .. } => "LaggingWal",
893 0 : ReconnectReason::SwitchAvailabilityZone => "SwitchAvailabilityZone",
894 0 : ReconnectReason::NoWalTimeout { .. } => "NoWalTimeout",
895 0 : ReconnectReason::NoKeepAlives { .. } => "NoKeepAlives",
896 : }
897 1703 : }
898 : }
899 :
900 : #[cfg(test)]
901 : mod tests {
902 : use super::*;
903 : use crate::tenant::harness::{TenantHarness, TIMELINE_ID};
904 : use url::Host;
905 :
906 38 : fn dummy_broker_sk_timeline(
907 38 : commit_lsn: u64,
908 38 : safekeeper_connstr: &str,
909 38 : latest_update: NaiveDateTime,
910 38 : ) -> BrokerSkTimeline {
911 38 : BrokerSkTimeline {
912 38 : timeline: SafekeeperTimelineInfo {
913 38 : safekeeper_id: 0,
914 38 : tenant_timeline_id: None,
915 38 : term: 0,
916 38 : last_log_term: 0,
917 38 : flush_lsn: 0,
918 38 : commit_lsn,
919 38 : backup_lsn: 0,
920 38 : remote_consistent_lsn: 0,
921 38 : peer_horizon_lsn: 0,
922 38 : local_start_lsn: 0,
923 38 : safekeeper_connstr: safekeeper_connstr.to_owned(),
924 38 : http_connstr: safekeeper_connstr.to_owned(),
925 38 : availability_zone: None,
926 38 : },
927 38 : latest_update,
928 38 : }
929 38 : }
930 :
931 2 : #[tokio::test]
932 2 : async fn no_connection_no_candidate() -> anyhow::Result<()> {
933 2 : let harness = TenantHarness::create("no_connection_no_candidate")?;
934 8 : let mut state = dummy_state(&harness).await;
935 2 : let now = Utc::now().naive_utc();
936 2 :
937 2 : let lagging_wal_timeout = chrono::Duration::from_std(state.conf.lagging_wal_timeout)?;
938 2 : let delay_over_threshold = now - lagging_wal_timeout - lagging_wal_timeout;
939 2 :
940 2 : state.wal_connection = None;
941 2 : state.wal_stream_candidates = HashMap::from([
942 2 : (NodeId(0), dummy_broker_sk_timeline(1, "", now)),
943 2 : (NodeId(1), dummy_broker_sk_timeline(0, "no_commit_lsn", now)),
944 2 : (NodeId(2), dummy_broker_sk_timeline(0, "no_commit_lsn", now)),
945 2 : (
946 2 : NodeId(3),
947 2 : dummy_broker_sk_timeline(
948 2 : 1 + state.conf.max_lsn_wal_lag.get(),
949 2 : "delay_over_threshold",
950 2 : delay_over_threshold,
951 2 : ),
952 2 : ),
953 2 : ]);
954 2 :
955 2 : let no_candidate = state.next_connection_candidate();
956 2 : assert!(
957 2 : no_candidate.is_none(),
958 2 : "Expected no candidate selected out of non full data options, but got {no_candidate:?}"
959 2 : );
960 2 :
961 2 : Ok(())
962 2 : }
963 :
964 2 : #[tokio::test]
965 2 : async fn connection_no_candidate() -> anyhow::Result<()> {
966 2 : let harness = TenantHarness::create("connection_no_candidate")?;
967 9 : let mut state = dummy_state(&harness).await;
968 2 : let now = Utc::now().naive_utc();
969 2 :
970 2 : let connected_sk_id = NodeId(0);
971 2 : let current_lsn = 100_000;
972 2 :
973 2 : let connection_status = WalConnectionStatus {
974 2 : is_connected: true,
975 2 : has_processed_wal: true,
976 2 : latest_connection_update: now,
977 2 : latest_wal_update: now,
978 2 : commit_lsn: Some(Lsn(current_lsn)),
979 2 : streaming_lsn: Some(Lsn(current_lsn)),
980 2 : node: NodeId(1),
981 2 : };
982 2 :
983 2 : state.conf.max_lsn_wal_lag = NonZeroU64::new(100).unwrap();
984 2 : state.wal_connection = Some(WalConnection {
985 2 : started_at: now,
986 2 : sk_id: connected_sk_id,
987 2 : availability_zone: None,
988 2 : status: connection_status,
989 2 : connection_task: TaskHandle::spawn(move |sender, _| async move {
990 2 : sender
991 2 : .send(TaskStateUpdate::Progress(connection_status))
992 2 : .ok();
993 2 : Ok(())
994 2 : }),
995 2 : discovered_new_wal: None,
996 2 : });
997 2 : state.wal_stream_candidates = HashMap::from([
998 2 : (
999 2 : connected_sk_id,
1000 2 : dummy_broker_sk_timeline(
1001 2 : current_lsn + state.conf.max_lsn_wal_lag.get() * 2,
1002 2 : DUMMY_SAFEKEEPER_HOST,
1003 2 : now,
1004 2 : ),
1005 2 : ),
1006 2 : (
1007 2 : NodeId(1),
1008 2 : dummy_broker_sk_timeline(current_lsn, "not_advanced_lsn", now),
1009 2 : ),
1010 2 : (
1011 2 : NodeId(2),
1012 2 : dummy_broker_sk_timeline(
1013 2 : current_lsn + state.conf.max_lsn_wal_lag.get() / 2,
1014 2 : "not_enough_advanced_lsn",
1015 2 : now,
1016 2 : ),
1017 2 : ),
1018 2 : ]);
1019 2 :
1020 2 : let no_candidate = state.next_connection_candidate();
1021 2 : assert!(
1022 2 : no_candidate.is_none(),
1023 2 : "Expected no candidate selected out of valid options since candidate Lsn data is ignored and others' was not advanced enough, but got {no_candidate:?}"
1024 2 : );
1025 2 :
1026 2 : Ok(())
1027 2 : }
1028 :
1029 2 : #[tokio::test]
1030 2 : async fn no_connection_candidate() -> anyhow::Result<()> {
1031 2 : let harness = TenantHarness::create("no_connection_candidate")?;
1032 9 : let mut state = dummy_state(&harness).await;
1033 2 : let now = Utc::now().naive_utc();
1034 2 :
1035 2 : state.wal_connection = None;
1036 2 : state.wal_stream_candidates = HashMap::from([(
1037 2 : NodeId(0),
1038 2 : dummy_broker_sk_timeline(
1039 2 : 1 + state.conf.max_lsn_wal_lag.get(),
1040 2 : DUMMY_SAFEKEEPER_HOST,
1041 2 : now,
1042 2 : ),
1043 2 : )]);
1044 2 :
1045 2 : let only_candidate = state
1046 2 : .next_connection_candidate()
1047 2 : .expect("Expected one candidate selected out of the only data option, but got none");
1048 2 : assert_eq!(only_candidate.safekeeper_id, NodeId(0));
1049 2 : assert_eq!(
1050 2 : only_candidate.reason,
1051 2 : ReconnectReason::NoExistingConnection,
1052 2 : "Should select new safekeeper due to missing connection, even if there's also a lag in the wal over the threshold"
1053 2 : );
1054 2 : assert_eq!(
1055 2 : only_candidate.wal_source_connconf.host(),
1056 2 : &Host::Domain(DUMMY_SAFEKEEPER_HOST.to_owned())
1057 2 : );
1058 2 :
1059 2 : let selected_lsn = 100_000;
1060 2 : state.wal_stream_candidates = HashMap::from([
1061 2 : (
1062 2 : NodeId(0),
1063 2 : dummy_broker_sk_timeline(selected_lsn - 100, "smaller_commit_lsn", now),
1064 2 : ),
1065 2 : (
1066 2 : NodeId(1),
1067 2 : dummy_broker_sk_timeline(selected_lsn, DUMMY_SAFEKEEPER_HOST, now),
1068 2 : ),
1069 2 : (
1070 2 : NodeId(2),
1071 2 : dummy_broker_sk_timeline(selected_lsn + 100, "", now),
1072 2 : ),
1073 2 : ]);
1074 2 : let biggest_wal_candidate = state.next_connection_candidate().expect(
1075 2 : "Expected one candidate selected out of multiple valid data options, but got none",
1076 2 : );
1077 2 :
1078 2 : assert_eq!(biggest_wal_candidate.safekeeper_id, NodeId(1));
1079 2 : assert_eq!(
1080 2 : biggest_wal_candidate.reason,
1081 2 : ReconnectReason::NoExistingConnection,
1082 2 : "Should select new safekeeper due to missing connection, even if there's also a lag in the wal over the threshold"
1083 2 : );
1084 2 : assert_eq!(
1085 2 : biggest_wal_candidate.wal_source_connconf.host(),
1086 2 : &Host::Domain(DUMMY_SAFEKEEPER_HOST.to_owned())
1087 2 : );
1088 2 :
1089 2 : Ok(())
1090 2 : }
1091 :
1092 2 : #[tokio::test]
1093 2 : async fn candidate_with_many_connection_failures() -> anyhow::Result<()> {
1094 2 : let harness = TenantHarness::create("candidate_with_many_connection_failures")?;
1095 9 : let mut state = dummy_state(&harness).await;
1096 2 : let now = Utc::now().naive_utc();
1097 2 :
1098 2 : let current_lsn = Lsn(100_000).align();
1099 2 : let bigger_lsn = Lsn(current_lsn.0 + 100).align();
1100 2 :
1101 2 : state.wal_connection = None;
1102 2 : state.wal_stream_candidates = HashMap::from([
1103 2 : (
1104 2 : NodeId(0),
1105 2 : dummy_broker_sk_timeline(bigger_lsn.0, DUMMY_SAFEKEEPER_HOST, now),
1106 2 : ),
1107 2 : (
1108 2 : NodeId(1),
1109 2 : dummy_broker_sk_timeline(current_lsn.0, DUMMY_SAFEKEEPER_HOST, now),
1110 2 : ),
1111 2 : ]);
1112 2 : state.wal_connection_retries = HashMap::from([(
1113 2 : NodeId(0),
1114 2 : RetryInfo {
1115 2 : next_retry_at: now.checked_add_signed(chrono::Duration::hours(1)),
1116 2 : retry_duration_seconds: WALCONNECTION_RETRY_MAX_BACKOFF_SECONDS,
1117 2 : },
1118 2 : )]);
1119 2 :
1120 2 : let candidate_with_less_errors = state
1121 2 : .next_connection_candidate()
1122 2 : .expect("Expected one candidate selected, but got none");
1123 2 : assert_eq!(
1124 2 : candidate_with_less_errors.safekeeper_id,
1125 2 : NodeId(1),
1126 2 : "Should select the node with no pending retry cooldown"
1127 2 : );
1128 2 :
1129 2 : Ok(())
1130 2 : }
1131 :
1132 2 : #[tokio::test]
1133 2 : async fn lsn_wal_over_threshold_current_candidate() -> anyhow::Result<()> {
1134 2 : let harness = TenantHarness::create("lsn_wal_over_threshcurrent_candidate")?;
1135 8 : let mut state = dummy_state(&harness).await;
1136 2 : let current_lsn = Lsn(100_000).align();
1137 2 : let now = Utc::now().naive_utc();
1138 2 :
1139 2 : let connected_sk_id = NodeId(0);
1140 2 : let new_lsn = Lsn(current_lsn.0 + state.conf.max_lsn_wal_lag.get() + 1);
1141 2 :
1142 2 : let connection_status = WalConnectionStatus {
1143 2 : is_connected: true,
1144 2 : has_processed_wal: true,
1145 2 : latest_connection_update: now,
1146 2 : latest_wal_update: now,
1147 2 : commit_lsn: Some(current_lsn),
1148 2 : streaming_lsn: Some(current_lsn),
1149 2 : node: connected_sk_id,
1150 2 : };
1151 2 :
1152 2 : state.wal_connection = Some(WalConnection {
1153 2 : started_at: now,
1154 2 : sk_id: connected_sk_id,
1155 2 : availability_zone: None,
1156 2 : status: connection_status,
1157 2 : connection_task: TaskHandle::spawn(move |sender, _| async move {
1158 2 : sender
1159 2 : .send(TaskStateUpdate::Progress(connection_status))
1160 2 : .ok();
1161 2 : Ok(())
1162 2 : }),
1163 2 : discovered_new_wal: None,
1164 2 : });
1165 2 : state.wal_stream_candidates = HashMap::from([
1166 2 : (
1167 2 : connected_sk_id,
1168 2 : dummy_broker_sk_timeline(current_lsn.0, DUMMY_SAFEKEEPER_HOST, now),
1169 2 : ),
1170 2 : (
1171 2 : NodeId(1),
1172 2 : dummy_broker_sk_timeline(new_lsn.0, "advanced_by_lsn_safekeeper", now),
1173 2 : ),
1174 2 : ]);
1175 2 :
1176 2 : let over_threshcurrent_candidate = state.next_connection_candidate().expect(
1177 2 : "Expected one candidate selected out of multiple valid data options, but got none",
1178 2 : );
1179 2 :
1180 2 : assert_eq!(over_threshcurrent_candidate.safekeeper_id, NodeId(1));
1181 2 : assert_eq!(
1182 2 : over_threshcurrent_candidate.reason,
1183 2 : ReconnectReason::LaggingWal {
1184 2 : current_commit_lsn: current_lsn,
1185 2 : new_commit_lsn: new_lsn,
1186 2 : threshold: state.conf.max_lsn_wal_lag
1187 2 : },
1188 2 : "Should select bigger WAL safekeeper if it starts to lag enough"
1189 2 : );
1190 2 : assert_eq!(
1191 2 : over_threshcurrent_candidate.wal_source_connconf.host(),
1192 2 : &Host::Domain("advanced_by_lsn_safekeeper".to_owned())
1193 2 : );
1194 2 :
1195 2 : Ok(())
1196 2 : }
1197 :
1198 2 : #[tokio::test]
1199 2 : async fn timeout_connection_threshold_current_candidate() -> anyhow::Result<()> {
1200 2 : let harness = TenantHarness::create("timeout_connection_threshold_current_candidate")?;
1201 8 : let mut state = dummy_state(&harness).await;
1202 2 : let current_lsn = Lsn(100_000).align();
1203 2 : let now = Utc::now().naive_utc();
1204 2 :
1205 2 : let wal_connect_timeout = chrono::Duration::from_std(state.conf.wal_connect_timeout)?;
1206 2 : let time_over_threshold =
1207 2 : Utc::now().naive_utc() - wal_connect_timeout - wal_connect_timeout;
1208 2 :
1209 2 : let connection_status = WalConnectionStatus {
1210 2 : is_connected: true,
1211 2 : has_processed_wal: true,
1212 2 : latest_connection_update: time_over_threshold,
1213 2 : latest_wal_update: time_over_threshold,
1214 2 : commit_lsn: Some(current_lsn),
1215 2 : streaming_lsn: Some(current_lsn),
1216 2 : node: NodeId(1),
1217 2 : };
1218 2 :
1219 2 : state.wal_connection = Some(WalConnection {
1220 2 : started_at: now,
1221 2 : sk_id: NodeId(1),
1222 2 : availability_zone: None,
1223 2 : status: connection_status,
1224 2 : connection_task: TaskHandle::spawn(move |sender, _| async move {
1225 2 : sender
1226 2 : .send(TaskStateUpdate::Progress(connection_status))
1227 2 : .ok();
1228 2 : Ok(())
1229 2 : }),
1230 2 : discovered_new_wal: None,
1231 2 : });
1232 2 : state.wal_stream_candidates = HashMap::from([(
1233 2 : NodeId(0),
1234 2 : dummy_broker_sk_timeline(current_lsn.0, DUMMY_SAFEKEEPER_HOST, now),
1235 2 : )]);
1236 2 :
1237 2 : let over_threshcurrent_candidate = state.next_connection_candidate().expect(
1238 2 : "Expected one candidate selected out of multiple valid data options, but got none",
1239 2 : );
1240 2 :
1241 2 : assert_eq!(over_threshcurrent_candidate.safekeeper_id, NodeId(0));
1242 2 : match over_threshcurrent_candidate.reason {
1243 2 : ReconnectReason::NoKeepAlives {
1244 2 : last_keep_alive,
1245 2 : threshold,
1246 2 : ..
1247 2 : } => {
1248 2 : assert_eq!(last_keep_alive, Some(time_over_threshold));
1249 2 : assert_eq!(threshold, state.conf.lagging_wal_timeout);
1250 2 : }
1251 2 : unexpected => panic!("Unexpected reason: {unexpected:?}"),
1252 2 : }
1253 2 : assert_eq!(
1254 2 : over_threshcurrent_candidate.wal_source_connconf.host(),
1255 2 : &Host::Domain(DUMMY_SAFEKEEPER_HOST.to_owned())
1256 2 : );
1257 2 :
1258 2 : Ok(())
1259 2 : }
1260 :
1261 2 : #[tokio::test]
1262 2 : async fn timeout_wal_over_threshold_current_candidate() -> anyhow::Result<()> {
1263 2 : let harness = TenantHarness::create("timeout_wal_over_threshold_current_candidate")?;
1264 8 : let mut state = dummy_state(&harness).await;
1265 2 : let current_lsn = Lsn(100_000).align();
1266 2 : let new_lsn = Lsn(100_100).align();
1267 2 : let now = Utc::now().naive_utc();
1268 2 :
1269 2 : let lagging_wal_timeout = chrono::Duration::from_std(state.conf.lagging_wal_timeout)?;
1270 2 : let time_over_threshold =
1271 2 : Utc::now().naive_utc() - lagging_wal_timeout - lagging_wal_timeout;
1272 2 :
1273 2 : let connection_status = WalConnectionStatus {
1274 2 : is_connected: true,
1275 2 : has_processed_wal: true,
1276 2 : latest_connection_update: now,
1277 2 : latest_wal_update: time_over_threshold,
1278 2 : commit_lsn: Some(current_lsn),
1279 2 : streaming_lsn: Some(current_lsn),
1280 2 : node: NodeId(1),
1281 2 : };
1282 2 :
1283 2 : state.wal_connection = Some(WalConnection {
1284 2 : started_at: now,
1285 2 : sk_id: NodeId(1),
1286 2 : availability_zone: None,
1287 2 : status: connection_status,
1288 2 : connection_task: TaskHandle::spawn(move |_, _| async move { Ok(()) }),
1289 2 : discovered_new_wal: Some(NewCommittedWAL {
1290 2 : discovered_at: time_over_threshold,
1291 2 : lsn: new_lsn,
1292 2 : }),
1293 2 : });
1294 2 : state.wal_stream_candidates = HashMap::from([(
1295 2 : NodeId(0),
1296 2 : dummy_broker_sk_timeline(new_lsn.0, DUMMY_SAFEKEEPER_HOST, now),
1297 2 : )]);
1298 2 :
1299 2 : let over_threshcurrent_candidate = state.next_connection_candidate().expect(
1300 2 : "Expected one candidate selected out of multiple valid data options, but got none",
1301 2 : );
1302 2 :
1303 2 : assert_eq!(over_threshcurrent_candidate.safekeeper_id, NodeId(0));
1304 2 : match over_threshcurrent_candidate.reason {
1305 2 : ReconnectReason::NoWalTimeout {
1306 2 : current_lsn,
1307 2 : current_commit_lsn,
1308 2 : candidate_commit_lsn,
1309 2 : last_wal_interaction,
1310 2 : threshold,
1311 2 : ..
1312 2 : } => {
1313 2 : assert_eq!(current_lsn, current_lsn);
1314 2 : assert_eq!(current_commit_lsn, current_lsn);
1315 2 : assert_eq!(candidate_commit_lsn, new_lsn);
1316 2 : assert_eq!(last_wal_interaction, Some(time_over_threshold));
1317 2 : assert_eq!(threshold, state.conf.lagging_wal_timeout);
1318 2 : }
1319 2 : unexpected => panic!("Unexpected reason: {unexpected:?}"),
1320 2 : }
1321 2 : assert_eq!(
1322 2 : over_threshcurrent_candidate.wal_source_connconf.host(),
1323 2 : &Host::Domain(DUMMY_SAFEKEEPER_HOST.to_owned())
1324 2 : );
1325 2 :
1326 2 : Ok(())
1327 2 : }
1328 :
1329 : const DUMMY_SAFEKEEPER_HOST: &str = "safekeeper_connstr";
1330 :
1331 16 : async fn dummy_state(harness: &TenantHarness) -> ConnectionManagerState {
1332 16 : let (tenant, ctx) = harness.load().await;
1333 16 : let timeline = tenant
1334 16 : .create_test_timeline(TIMELINE_ID, Lsn(0x8), crate::DEFAULT_PG_VERSION, &ctx)
1335 52 : .await
1336 16 : .expect("Failed to create an empty timeline for dummy wal connection manager");
1337 16 :
1338 16 : ConnectionManagerState {
1339 16 : id: TenantTimelineId {
1340 16 : tenant_id: harness.tenant_shard_id.tenant_id,
1341 16 : timeline_id: TIMELINE_ID,
1342 16 : },
1343 16 : timeline,
1344 16 : conf: WalReceiverConf {
1345 16 : wal_connect_timeout: Duration::from_secs(1),
1346 16 : lagging_wal_timeout: Duration::from_secs(1),
1347 16 : max_lsn_wal_lag: NonZeroU64::new(1024 * 1024).unwrap(),
1348 16 : auth_token: None,
1349 16 : availability_zone: None,
1350 16 : ingest_batch_size: 1,
1351 16 : },
1352 16 : wal_connection: None,
1353 16 : wal_stream_candidates: HashMap::new(),
1354 16 : wal_connection_retries: HashMap::new(),
1355 16 : }
1356 16 : }
1357 :
1358 2 : #[tokio::test]
1359 2 : async fn switch_to_same_availability_zone() -> anyhow::Result<()> {
1360 2 : // Pageserver and one of safekeepers will be in the same availability zone
1361 2 : // and pageserver should prefer to connect to it.
1362 2 : let test_az = Some("test_az".to_owned());
1363 2 :
1364 2 : let harness = TenantHarness::create("switch_to_same_availability_zone")?;
1365 9 : let mut state = dummy_state(&harness).await;
1366 2 : state.conf.availability_zone = test_az.clone();
1367 2 : let current_lsn = Lsn(100_000).align();
1368 2 : let now = Utc::now().naive_utc();
1369 2 :
1370 2 : let connected_sk_id = NodeId(0);
1371 2 :
1372 2 : let connection_status = WalConnectionStatus {
1373 2 : is_connected: true,
1374 2 : has_processed_wal: true,
1375 2 : latest_connection_update: now,
1376 2 : latest_wal_update: now,
1377 2 : commit_lsn: Some(current_lsn),
1378 2 : streaming_lsn: Some(current_lsn),
1379 2 : node: connected_sk_id,
1380 2 : };
1381 2 :
1382 2 : state.wal_connection = Some(WalConnection {
1383 2 : started_at: now,
1384 2 : sk_id: connected_sk_id,
1385 2 : availability_zone: None,
1386 2 : status: connection_status,
1387 2 : connection_task: TaskHandle::spawn(move |sender, _| async move {
1388 2 : sender
1389 2 : .send(TaskStateUpdate::Progress(connection_status))
1390 2 : .ok();
1391 2 : Ok(())
1392 2 : }),
1393 2 : discovered_new_wal: None,
1394 2 : });
1395 2 :
1396 2 : // We have another safekeeper with the same commit_lsn, and it have the same availability zone as
1397 2 : // the current pageserver.
1398 2 : let mut same_az_sk = dummy_broker_sk_timeline(current_lsn.0, "same_az", now);
1399 2 : same_az_sk.timeline.availability_zone = test_az.clone();
1400 2 :
1401 2 : state.wal_stream_candidates = HashMap::from([
1402 2 : (
1403 2 : connected_sk_id,
1404 2 : dummy_broker_sk_timeline(current_lsn.0, DUMMY_SAFEKEEPER_HOST, now),
1405 2 : ),
1406 2 : (NodeId(1), same_az_sk),
1407 2 : ]);
1408 2 :
1409 2 : // We expect that pageserver will switch to the safekeeper in the same availability zone,
1410 2 : // even if it has the same commit_lsn.
1411 2 : let next_candidate = state.next_connection_candidate().expect(
1412 2 : "Expected one candidate selected out of multiple valid data options, but got none",
1413 2 : );
1414 2 :
1415 2 : assert_eq!(next_candidate.safekeeper_id, NodeId(1));
1416 2 : assert_eq!(
1417 2 : next_candidate.reason,
1418 2 : ReconnectReason::SwitchAvailabilityZone,
1419 2 : "Should switch to the safekeeper in the same availability zone, if it has the same commit_lsn"
1420 2 : );
1421 2 : assert_eq!(
1422 2 : next_candidate.wal_source_connconf.host(),
1423 2 : &Host::Domain("same_az".to_owned())
1424 2 : );
1425 2 :
1426 2 : Ok(())
1427 2 : }
1428 : }
|