Line data Source code
1 : use futures::StreamExt;
2 : use std::{str::FromStr, time::Duration};
3 :
4 : use clap::{Parser, Subcommand};
5 : use pageserver_api::{
6 : controller_api::{
7 : AvailabilityZone, NodeAvailabilityWrapper, NodeDescribeResponse, NodeShardResponse,
8 : ShardSchedulingPolicy, TenantCreateRequest, TenantDescribeResponse, TenantPolicyRequest,
9 : },
10 : models::{
11 : EvictionPolicy, EvictionPolicyLayerAccessThreshold, LocationConfigSecondary,
12 : ShardParameters, TenantConfig, TenantConfigRequest, TenantShardSplitRequest,
13 : TenantShardSplitResponse,
14 : },
15 : shard::{ShardStripeSize, TenantShardId},
16 : };
17 : use pageserver_client::mgmt_api::{self};
18 : use reqwest::{Method, StatusCode, Url};
19 : use utils::id::{NodeId, TenantId};
20 :
21 : use pageserver_api::controller_api::{
22 : NodeConfigureRequest, NodeRegisterRequest, NodeSchedulingPolicy, PlacementPolicy,
23 : TenantShardMigrateRequest, TenantShardMigrateResponse,
24 : };
25 : use storage_controller_client::control_api::Client;
26 :
27 : #[derive(Subcommand, Debug)]
28 : enum Command {
29 : /// Register a pageserver with the storage controller. This shouldn't usually be necessary,
30 : /// since pageservers auto-register when they start up
31 : NodeRegister {
32 : #[arg(long)]
33 0 : node_id: NodeId,
34 :
35 : #[arg(long)]
36 0 : listen_pg_addr: String,
37 : #[arg(long)]
38 0 : listen_pg_port: u16,
39 :
40 : #[arg(long)]
41 0 : listen_http_addr: String,
42 : #[arg(long)]
43 0 : listen_http_port: u16,
44 : #[arg(long)]
45 0 : availability_zone_id: String,
46 : },
47 :
48 : /// Modify a node's configuration in the storage controller
49 : NodeConfigure {
50 : #[arg(long)]
51 0 : node_id: NodeId,
52 :
53 : /// Availability is usually auto-detected based on heartbeats. Set 'offline' here to
54 : /// manually mark a node offline
55 : #[arg(long)]
56 : availability: Option<NodeAvailabilityArg>,
57 : /// Scheduling policy controls whether tenant shards may be scheduled onto this node.
58 : #[arg(long)]
59 : scheduling: Option<NodeSchedulingPolicy>,
60 : },
61 : NodeDelete {
62 : #[arg(long)]
63 0 : node_id: NodeId,
64 : },
65 : /// Modify a tenant's policies in the storage controller
66 : TenantPolicy {
67 : #[arg(long)]
68 0 : tenant_id: TenantId,
69 : /// Placement policy controls whether a tenant is `detached`, has only a secondary location (`secondary`),
70 : /// or is in the normal attached state with N secondary locations (`attached:N`)
71 : #[arg(long)]
72 : placement: Option<PlacementPolicyArg>,
73 : /// Scheduling policy enables pausing the controller's scheduling activity involving this tenant. `active` is normal,
74 : /// `essential` disables optimization scheduling changes, `pause` disables all scheduling changes, and `stop` prevents
75 : /// all reconciliation activity including for scheduling changes already made. `pause` and `stop` can make a tenant
76 : /// unavailable, and are only for use in emergencies.
77 : #[arg(long)]
78 : scheduling: Option<ShardSchedulingPolicyArg>,
79 : },
80 : /// List nodes known to the storage controller
81 : Nodes {},
82 : /// List tenants known to the storage controller
83 : Tenants {
84 : /// If this field is set, it will list the tenants on a specific node
85 : node_id: Option<NodeId>,
86 : },
87 : /// Create a new tenant in the storage controller, and by extension on pageservers.
88 : TenantCreate {
89 : #[arg(long)]
90 0 : tenant_id: TenantId,
91 : },
92 : /// Delete a tenant in the storage controller, and by extension on pageservers.
93 : TenantDelete {
94 : #[arg(long)]
95 0 : tenant_id: TenantId,
96 : },
97 : /// Split an existing tenant into a higher number of shards than its current shard count.
98 : TenantShardSplit {
99 : #[arg(long)]
100 0 : tenant_id: TenantId,
101 : #[arg(long)]
102 0 : shard_count: u8,
103 : /// Optional, in 8kiB pages. e.g. set 2048 for 16MB stripes.
104 : #[arg(long)]
105 : stripe_size: Option<u32>,
106 : },
107 : /// Migrate the attached location for a tenant shard to a specific pageserver.
108 : TenantShardMigrate {
109 : #[arg(long)]
110 0 : tenant_shard_id: TenantShardId,
111 : #[arg(long)]
112 0 : node: NodeId,
113 : },
114 : /// Cancel any ongoing reconciliation for this shard
115 : TenantShardCancelReconcile {
116 : #[arg(long)]
117 0 : tenant_shard_id: TenantShardId,
118 : },
119 : /// Modify the pageserver tenant configuration of a tenant: this is the configuration structure
120 : /// that is passed through to pageservers, and does not affect storage controller behavior.
121 : TenantConfig {
122 : #[arg(long)]
123 0 : tenant_id: TenantId,
124 : #[arg(long)]
125 0 : config: String,
126 : },
127 : /// Print details about a particular tenant, including all its shards' states.
128 : TenantDescribe {
129 : #[arg(long)]
130 0 : tenant_id: TenantId,
131 : },
132 : /// For a tenant which hasn't been onboarded to the storage controller yet, add it in secondary
133 : /// mode so that it can warm up content on a pageserver.
134 : TenantWarmup {
135 : #[arg(long)]
136 0 : tenant_id: TenantId,
137 : },
138 : /// Uncleanly drop a tenant from the storage controller: this doesn't delete anything from pageservers. Appropriate
139 : /// if you e.g. used `tenant-warmup` by mistake on a tenant ID that doesn't really exist, or is in some other region.
140 : TenantDrop {
141 : #[arg(long)]
142 0 : tenant_id: TenantId,
143 : #[arg(long)]
144 0 : unclean: bool,
145 : },
146 : NodeDrop {
147 : #[arg(long)]
148 0 : node_id: NodeId,
149 : #[arg(long)]
150 0 : unclean: bool,
151 : },
152 : TenantSetTimeBasedEviction {
153 : #[arg(long)]
154 0 : tenant_id: TenantId,
155 : #[arg(long)]
156 0 : period: humantime::Duration,
157 : #[arg(long)]
158 0 : threshold: humantime::Duration,
159 : },
160 : // Migrate away from a set of specified pageservers by moving the primary attachments to pageservers
161 : // outside of the specified set.
162 : BulkMigrate {
163 : // Set of pageserver node ids to drain.
164 : #[arg(long)]
165 0 : nodes: Vec<NodeId>,
166 : // Optional: migration concurrency (default is 8)
167 : #[arg(long)]
168 : concurrency: Option<usize>,
169 : // Optional: maximum number of shards to migrate
170 : #[arg(long)]
171 : max_shards: Option<usize>,
172 : // Optional: when set to true, nothing is migrated, but the plan is printed to stdout
173 : #[arg(long)]
174 : dry_run: Option<bool>,
175 : },
176 : /// Start draining the specified pageserver.
177 : /// The drain is complete when the schedulling policy returns to active.
178 : StartDrain {
179 : #[arg(long)]
180 0 : node_id: NodeId,
181 : },
182 : /// Cancel draining the specified pageserver and wait for `timeout`
183 : /// for the operation to be canceled. May be retried.
184 : CancelDrain {
185 : #[arg(long)]
186 0 : node_id: NodeId,
187 : #[arg(long)]
188 0 : timeout: humantime::Duration,
189 : },
190 : /// Start filling the specified pageserver.
191 : /// The drain is complete when the schedulling policy returns to active.
192 : StartFill {
193 : #[arg(long)]
194 0 : node_id: NodeId,
195 : },
196 : /// Cancel filling the specified pageserver and wait for `timeout`
197 : /// for the operation to be canceled. May be retried.
198 : CancelFill {
199 : #[arg(long)]
200 0 : node_id: NodeId,
201 : #[arg(long)]
202 0 : timeout: humantime::Duration,
203 : },
204 : }
205 :
206 : #[derive(Parser)]
207 : #[command(
208 : author,
209 : version,
210 : about,
211 : long_about = "CLI for Storage Controller Support/Debug"
212 : )]
213 : #[command(arg_required_else_help(true))]
214 : struct Cli {
215 : #[arg(long)]
216 : /// URL to storage controller. e.g. http://127.0.0.1:1234 when using `neon_local`
217 0 : api: Url,
218 :
219 : #[arg(long)]
220 : /// JWT token for authenticating with storage controller. Depending on the API used, this
221 : /// should have either `pageserverapi` or `admin` scopes: for convenience, you should mint
222 : /// a token with both scopes to use with this tool.
223 : jwt: Option<String>,
224 :
225 : #[command(subcommand)]
226 : command: Command,
227 : }
228 :
229 : #[derive(Debug, Clone)]
230 : struct PlacementPolicyArg(PlacementPolicy);
231 :
232 : impl FromStr for PlacementPolicyArg {
233 : type Err = anyhow::Error;
234 :
235 0 : fn from_str(s: &str) -> Result<Self, Self::Err> {
236 0 : match s {
237 0 : "detached" => Ok(Self(PlacementPolicy::Detached)),
238 0 : "secondary" => Ok(Self(PlacementPolicy::Secondary)),
239 0 : _ if s.starts_with("attached:") => {
240 0 : let mut splitter = s.split(':');
241 0 : let _prefix = splitter.next().unwrap();
242 0 : match splitter.next().and_then(|s| s.parse::<usize>().ok()) {
243 0 : Some(n) => Ok(Self(PlacementPolicy::Attached(n))),
244 0 : None => Err(anyhow::anyhow!(
245 0 : "Invalid format '{s}', a valid example is 'attached:1'"
246 0 : )),
247 : }
248 : }
249 0 : _ => Err(anyhow::anyhow!(
250 0 : "Unknown placement policy '{s}', try detached,secondary,attached:<n>"
251 0 : )),
252 : }
253 0 : }
254 : }
255 :
256 : #[derive(Debug, Clone)]
257 : struct ShardSchedulingPolicyArg(ShardSchedulingPolicy);
258 :
259 : impl FromStr for ShardSchedulingPolicyArg {
260 : type Err = anyhow::Error;
261 :
262 0 : fn from_str(s: &str) -> Result<Self, Self::Err> {
263 0 : match s {
264 0 : "active" => Ok(Self(ShardSchedulingPolicy::Active)),
265 0 : "essential" => Ok(Self(ShardSchedulingPolicy::Essential)),
266 0 : "pause" => Ok(Self(ShardSchedulingPolicy::Pause)),
267 0 : "stop" => Ok(Self(ShardSchedulingPolicy::Stop)),
268 0 : _ => Err(anyhow::anyhow!(
269 0 : "Unknown scheduling policy '{s}', try active,essential,pause,stop"
270 0 : )),
271 : }
272 0 : }
273 : }
274 :
275 : #[derive(Debug, Clone)]
276 : struct NodeAvailabilityArg(NodeAvailabilityWrapper);
277 :
278 : impl FromStr for NodeAvailabilityArg {
279 : type Err = anyhow::Error;
280 :
281 0 : fn from_str(s: &str) -> Result<Self, Self::Err> {
282 0 : match s {
283 0 : "active" => Ok(Self(NodeAvailabilityWrapper::Active)),
284 0 : "offline" => Ok(Self(NodeAvailabilityWrapper::Offline)),
285 0 : _ => Err(anyhow::anyhow!("Unknown availability state '{s}'")),
286 : }
287 0 : }
288 : }
289 :
290 0 : async fn wait_for_scheduling_policy<F>(
291 0 : client: Client,
292 0 : node_id: NodeId,
293 0 : timeout: Duration,
294 0 : f: F,
295 0 : ) -> anyhow::Result<NodeSchedulingPolicy>
296 0 : where
297 0 : F: Fn(NodeSchedulingPolicy) -> bool,
298 0 : {
299 0 : let waiter = tokio::time::timeout(timeout, async move {
300 : loop {
301 0 : let node = client
302 0 : .dispatch::<(), NodeDescribeResponse>(
303 0 : Method::GET,
304 0 : format!("control/v1/node/{node_id}"),
305 0 : None,
306 0 : )
307 0 : .await?;
308 :
309 0 : if f(node.scheduling) {
310 0 : return Ok::<NodeSchedulingPolicy, mgmt_api::Error>(node.scheduling);
311 0 : }
312 : }
313 0 : });
314 0 :
315 0 : Ok(waiter.await??)
316 0 : }
317 :
318 : #[tokio::main]
319 0 : async fn main() -> anyhow::Result<()> {
320 0 : let cli = Cli::parse();
321 0 :
322 0 : let storcon_client = Client::new(cli.api.clone(), cli.jwt.clone());
323 0 :
324 0 : let mut trimmed = cli.api.to_string();
325 0 : trimmed.pop();
326 0 : let vps_client = mgmt_api::Client::new(trimmed, cli.jwt.as_deref());
327 0 :
328 0 : match cli.command {
329 0 : Command::NodeRegister {
330 0 : node_id,
331 0 : listen_pg_addr,
332 0 : listen_pg_port,
333 0 : listen_http_addr,
334 0 : listen_http_port,
335 0 : availability_zone_id,
336 0 : } => {
337 0 : storcon_client
338 0 : .dispatch::<_, ()>(
339 0 : Method::POST,
340 0 : "control/v1/node".to_string(),
341 0 : Some(NodeRegisterRequest {
342 0 : node_id,
343 0 : listen_pg_addr,
344 0 : listen_pg_port,
345 0 : listen_http_addr,
346 0 : listen_http_port,
347 0 : availability_zone_id: AvailabilityZone(availability_zone_id),
348 0 : }),
349 0 : )
350 0 : .await?;
351 0 : }
352 0 : Command::TenantCreate { tenant_id } => {
353 0 : storcon_client
354 0 : .dispatch::<_, ()>(
355 0 : Method::POST,
356 0 : "v1/tenant".to_string(),
357 0 : Some(TenantCreateRequest {
358 0 : new_tenant_id: TenantShardId::unsharded(tenant_id),
359 0 : generation: None,
360 0 : shard_parameters: ShardParameters::default(),
361 0 : placement_policy: Some(PlacementPolicy::Attached(1)),
362 0 : config: TenantConfig::default(),
363 0 : }),
364 0 : )
365 0 : .await?;
366 0 : }
367 0 : Command::TenantDelete { tenant_id } => {
368 0 : let status = vps_client
369 0 : .tenant_delete(TenantShardId::unsharded(tenant_id))
370 0 : .await?;
371 0 : tracing::info!("Delete status: {}", status);
372 0 : }
373 0 : Command::Nodes {} => {
374 0 : let mut resp = storcon_client
375 0 : .dispatch::<(), Vec<NodeDescribeResponse>>(
376 0 : Method::GET,
377 0 : "control/v1/node".to_string(),
378 0 : None,
379 0 : )
380 0 : .await?;
381 0 :
382 0 : resp.sort_by(|a, b| a.listen_http_addr.cmp(&b.listen_http_addr));
383 0 :
384 0 : let mut table = comfy_table::Table::new();
385 0 : table.set_header(["Id", "Hostname", "Scheduling", "Availability"]);
386 0 : for node in resp {
387 0 : table.add_row([
388 0 : format!("{}", node.id),
389 0 : node.listen_http_addr,
390 0 : format!("{:?}", node.scheduling),
391 0 : format!("{:?}", node.availability),
392 0 : ]);
393 0 : }
394 0 : println!("{table}");
395 0 : }
396 0 : Command::NodeConfigure {
397 0 : node_id,
398 0 : availability,
399 0 : scheduling,
400 0 : } => {
401 0 : let req = NodeConfigureRequest {
402 0 : node_id,
403 0 : availability: availability.map(|a| a.0),
404 0 : scheduling,
405 0 : };
406 0 : storcon_client
407 0 : .dispatch::<_, ()>(
408 0 : Method::PUT,
409 0 : format!("control/v1/node/{node_id}/config"),
410 0 : Some(req),
411 0 : )
412 0 : .await?;
413 0 : }
414 0 : Command::Tenants {
415 0 : node_id: Some(node_id),
416 0 : } => {
417 0 : let describe_response = storcon_client
418 0 : .dispatch::<(), NodeShardResponse>(
419 0 : Method::GET,
420 0 : format!("control/v1/node/{node_id}/shards"),
421 0 : None,
422 0 : )
423 0 : .await?;
424 0 : let shards = describe_response.shards;
425 0 : let mut table = comfy_table::Table::new();
426 0 : table.set_header([
427 0 : "Shard",
428 0 : "Intended Primary/Secondary",
429 0 : "Observed Primary/Secondary",
430 0 : ]);
431 0 : for shard in shards {
432 0 : table.add_row([
433 0 : format!("{}", shard.tenant_shard_id),
434 0 : match shard.is_intended_secondary {
435 0 : None => "".to_string(),
436 0 : Some(true) => "Secondary".to_string(),
437 0 : Some(false) => "Primary".to_string(),
438 0 : },
439 0 : match shard.is_observed_secondary {
440 0 : None => "".to_string(),
441 0 : Some(true) => "Secondary".to_string(),
442 0 : Some(false) => "Primary".to_string(),
443 0 : },
444 0 : ]);
445 0 : }
446 0 : println!("{table}");
447 0 : }
448 0 : Command::Tenants { node_id: None } => {
449 0 : let mut resp = storcon_client
450 0 : .dispatch::<(), Vec<TenantDescribeResponse>>(
451 0 : Method::GET,
452 0 : "control/v1/tenant".to_string(),
453 0 : None,
454 0 : )
455 0 : .await?;
456 0 :
457 0 : resp.sort_by(|a, b| a.tenant_id.cmp(&b.tenant_id));
458 0 :
459 0 : let mut table = comfy_table::Table::new();
460 0 : table.set_header([
461 0 : "TenantId",
462 0 : "ShardCount",
463 0 : "StripeSize",
464 0 : "Placement",
465 0 : "Scheduling",
466 0 : ]);
467 0 : for tenant in resp {
468 0 : let shard_zero = tenant.shards.into_iter().next().unwrap();
469 0 : table.add_row([
470 0 : format!("{}", tenant.tenant_id),
471 0 : format!("{}", shard_zero.tenant_shard_id.shard_count.literal()),
472 0 : format!("{:?}", tenant.stripe_size),
473 0 : format!("{:?}", tenant.policy),
474 0 : format!("{:?}", shard_zero.scheduling_policy),
475 0 : ]);
476 0 : }
477 0 :
478 0 : println!("{table}");
479 0 : }
480 0 : Command::TenantPolicy {
481 0 : tenant_id,
482 0 : placement,
483 0 : scheduling,
484 0 : } => {
485 0 : let req = TenantPolicyRequest {
486 0 : scheduling: scheduling.map(|s| s.0),
487 0 : placement: placement.map(|p| p.0),
488 0 : };
489 0 : storcon_client
490 0 : .dispatch::<_, ()>(
491 0 : Method::PUT,
492 0 : format!("control/v1/tenant/{tenant_id}/policy"),
493 0 : Some(req),
494 0 : )
495 0 : .await?;
496 0 : }
497 0 : Command::TenantShardSplit {
498 0 : tenant_id,
499 0 : shard_count,
500 0 : stripe_size,
501 0 : } => {
502 0 : let req = TenantShardSplitRequest {
503 0 : new_shard_count: shard_count,
504 0 : new_stripe_size: stripe_size.map(ShardStripeSize),
505 0 : };
506 0 :
507 0 : let response = storcon_client
508 0 : .dispatch::<TenantShardSplitRequest, TenantShardSplitResponse>(
509 0 : Method::PUT,
510 0 : format!("control/v1/tenant/{tenant_id}/shard_split"),
511 0 : Some(req),
512 0 : )
513 0 : .await?;
514 0 : println!(
515 0 : "Split tenant {} into {} shards: {}",
516 0 : tenant_id,
517 0 : shard_count,
518 0 : response
519 0 : .new_shards
520 0 : .iter()
521 0 : .map(|s| format!("{:?}", s))
522 0 : .collect::<Vec<_>>()
523 0 : .join(",")
524 0 : );
525 0 : }
526 0 : Command::TenantShardMigrate {
527 0 : tenant_shard_id,
528 0 : node,
529 0 : } => {
530 0 : let req = TenantShardMigrateRequest {
531 0 : tenant_shard_id,
532 0 : node_id: node,
533 0 : };
534 0 :
535 0 : storcon_client
536 0 : .dispatch::<TenantShardMigrateRequest, TenantShardMigrateResponse>(
537 0 : Method::PUT,
538 0 : format!("control/v1/tenant/{tenant_shard_id}/migrate"),
539 0 : Some(req),
540 0 : )
541 0 : .await?;
542 0 : }
543 0 : Command::TenantShardCancelReconcile { tenant_shard_id } => {
544 0 : storcon_client
545 0 : .dispatch::<(), ()>(
546 0 : Method::PUT,
547 0 : format!("control/v1/tenant/{tenant_shard_id}/cancel_reconcile"),
548 0 : None,
549 0 : )
550 0 : .await?;
551 0 : }
552 0 : Command::TenantConfig { tenant_id, config } => {
553 0 : let tenant_conf = serde_json::from_str(&config)?;
554 0 :
555 0 : vps_client
556 0 : .tenant_config(&TenantConfigRequest {
557 0 : tenant_id,
558 0 : config: tenant_conf,
559 0 : })
560 0 : .await?;
561 0 : }
562 0 : Command::TenantDescribe { tenant_id } => {
563 0 : let TenantDescribeResponse {
564 0 : tenant_id,
565 0 : shards,
566 0 : stripe_size,
567 0 : policy,
568 0 : config,
569 0 : } = storcon_client
570 0 : .dispatch::<(), TenantDescribeResponse>(
571 0 : Method::GET,
572 0 : format!("control/v1/tenant/{tenant_id}"),
573 0 : None,
574 0 : )
575 0 : .await?;
576 0 : println!("Tenant {tenant_id}");
577 0 : let mut table = comfy_table::Table::new();
578 0 : table.add_row(["Policy", &format!("{:?}", policy)]);
579 0 : table.add_row(["Stripe size", &format!("{:?}", stripe_size)]);
580 0 : table.add_row(["Config", &serde_json::to_string_pretty(&config).unwrap()]);
581 0 : println!("{table}");
582 0 : println!("Shards:");
583 0 : let mut table = comfy_table::Table::new();
584 0 : table.set_header(["Shard", "Attached", "Secondary", "Last error", "status"]);
585 0 : for shard in shards {
586 0 : let secondary = shard
587 0 : .node_secondary
588 0 : .iter()
589 0 : .map(|n| format!("{}", n))
590 0 : .collect::<Vec<_>>()
591 0 : .join(",");
592 0 :
593 0 : let mut status_parts = Vec::new();
594 0 : if shard.is_reconciling {
595 0 : status_parts.push("reconciling");
596 0 : }
597 0 :
598 0 : if shard.is_pending_compute_notification {
599 0 : status_parts.push("pending_compute");
600 0 : }
601 0 :
602 0 : if shard.is_splitting {
603 0 : status_parts.push("splitting");
604 0 : }
605 0 : let status = status_parts.join(",");
606 0 :
607 0 : table.add_row([
608 0 : format!("{}", shard.tenant_shard_id),
609 0 : shard
610 0 : .node_attached
611 0 : .map(|n| format!("{}", n))
612 0 : .unwrap_or(String::new()),
613 0 : secondary,
614 0 : shard.last_error,
615 0 : status,
616 0 : ]);
617 0 : }
618 0 : println!("{table}");
619 0 : }
620 0 : Command::TenantWarmup { tenant_id } => {
621 0 : let describe_response = storcon_client
622 0 : .dispatch::<(), TenantDescribeResponse>(
623 0 : Method::GET,
624 0 : format!("control/v1/tenant/{tenant_id}"),
625 0 : None,
626 0 : )
627 0 : .await;
628 0 : match describe_response {
629 0 : Ok(describe) => {
630 0 : if matches!(describe.policy, PlacementPolicy::Secondary) {
631 0 : // Fine: it's already known to controller in secondary mode: calling
632 0 : // again to put it into secondary mode won't cause problems.
633 0 : } else {
634 0 : anyhow::bail!("Tenant already present with policy {:?}", describe.policy);
635 0 : }
636 0 : }
637 0 : Err(mgmt_api::Error::ApiError(StatusCode::NOT_FOUND, _)) => {
638 0 : // Fine: this tenant isn't know to the storage controller yet.
639 0 : }
640 0 : Err(e) => {
641 0 : // Unexpected API error
642 0 : return Err(e.into());
643 0 : }
644 0 : }
645 0 :
646 0 : vps_client
647 0 : .location_config(
648 0 : TenantShardId::unsharded(tenant_id),
649 0 : pageserver_api::models::LocationConfig {
650 0 : mode: pageserver_api::models::LocationConfigMode::Secondary,
651 0 : generation: None,
652 0 : secondary_conf: Some(LocationConfigSecondary { warm: true }),
653 0 : shard_number: 0,
654 0 : shard_count: 0,
655 0 : shard_stripe_size: ShardParameters::DEFAULT_STRIPE_SIZE.0,
656 0 : tenant_conf: TenantConfig::default(),
657 0 : },
658 0 : None,
659 0 : true,
660 0 : )
661 0 : .await?;
662 0 :
663 0 : let describe_response = storcon_client
664 0 : .dispatch::<(), TenantDescribeResponse>(
665 0 : Method::GET,
666 0 : format!("control/v1/tenant/{tenant_id}"),
667 0 : None,
668 0 : )
669 0 : .await?;
670 0 :
671 0 : let secondary_ps_id = describe_response
672 0 : .shards
673 0 : .first()
674 0 : .unwrap()
675 0 : .node_secondary
676 0 : .first()
677 0 : .unwrap();
678 0 :
679 0 : println!("Tenant {tenant_id} warming up on pageserver {secondary_ps_id}");
680 0 : loop {
681 0 : let (status, progress) = vps_client
682 0 : .tenant_secondary_download(
683 0 : TenantShardId::unsharded(tenant_id),
684 0 : Some(Duration::from_secs(10)),
685 0 : )
686 0 : .await?;
687 0 : println!(
688 0 : "Progress: {}/{} layers, {}/{} bytes",
689 0 : progress.layers_downloaded,
690 0 : progress.layers_total,
691 0 : progress.bytes_downloaded,
692 0 : progress.bytes_total
693 0 : );
694 0 : match status {
695 0 : StatusCode::OK => {
696 0 : println!("Download complete");
697 0 : break;
698 0 : }
699 0 : StatusCode::ACCEPTED => {
700 0 : // Loop
701 0 : }
702 0 : _ => {
703 0 : anyhow::bail!("Unexpected download status: {status}");
704 0 : }
705 0 : }
706 0 : }
707 0 : }
708 0 : Command::TenantDrop { tenant_id, unclean } => {
709 0 : if !unclean {
710 0 : anyhow::bail!("This command is not a tenant deletion, and uncleanly drops all controller state for the tenant. If you know what you're doing, add `--unclean` to proceed.")
711 0 : }
712 0 : storcon_client
713 0 : .dispatch::<(), ()>(
714 0 : Method::POST,
715 0 : format!("debug/v1/tenant/{tenant_id}/drop"),
716 0 : None,
717 0 : )
718 0 : .await?;
719 0 : }
720 0 : Command::NodeDrop { node_id, unclean } => {
721 0 : if !unclean {
722 0 : anyhow::bail!("This command is not a clean node decommission, and uncleanly drops all controller state for the node, without checking if any tenants still refer to it. If you know what you're doing, add `--unclean` to proceed.")
723 0 : }
724 0 : storcon_client
725 0 : .dispatch::<(), ()>(Method::POST, format!("debug/v1/node/{node_id}/drop"), None)
726 0 : .await?;
727 0 : }
728 0 : Command::NodeDelete { node_id } => {
729 0 : storcon_client
730 0 : .dispatch::<(), ()>(Method::DELETE, format!("control/v1/node/{node_id}"), None)
731 0 : .await?;
732 0 : }
733 0 : Command::TenantSetTimeBasedEviction {
734 0 : tenant_id,
735 0 : period,
736 0 : threshold,
737 0 : } => {
738 0 : vps_client
739 0 : .tenant_config(&TenantConfigRequest {
740 0 : tenant_id,
741 0 : config: TenantConfig {
742 0 : eviction_policy: Some(EvictionPolicy::LayerAccessThreshold(
743 0 : EvictionPolicyLayerAccessThreshold {
744 0 : period: period.into(),
745 0 : threshold: threshold.into(),
746 0 : },
747 0 : )),
748 0 : heatmap_period: Some("300s".to_string()),
749 0 : ..Default::default()
750 0 : },
751 0 : })
752 0 : .await?;
753 0 : }
754 0 : Command::BulkMigrate {
755 0 : nodes,
756 0 : concurrency,
757 0 : max_shards,
758 0 : dry_run,
759 0 : } => {
760 0 : // Load the list of nodes, split them up into the drained and filled sets,
761 0 : // and validate that draining is possible.
762 0 : let node_descs = storcon_client
763 0 : .dispatch::<(), Vec<NodeDescribeResponse>>(
764 0 : Method::GET,
765 0 : "control/v1/node".to_string(),
766 0 : None,
767 0 : )
768 0 : .await?;
769 0 :
770 0 : let mut node_to_drain_descs = Vec::new();
771 0 : let mut node_to_fill_descs = Vec::new();
772 0 :
773 0 : for desc in node_descs {
774 0 : let to_drain = nodes.iter().any(|id| *id == desc.id);
775 0 : if to_drain {
776 0 : node_to_drain_descs.push(desc);
777 0 : } else {
778 0 : node_to_fill_descs.push(desc);
779 0 : }
780 0 : }
781 0 :
782 0 : if nodes.len() != node_to_drain_descs.len() {
783 0 : anyhow::bail!("Bulk migration requested away from node which doesn't exist.")
784 0 : }
785 0 :
786 0 : node_to_fill_descs.retain(|desc| {
787 0 : matches!(desc.availability, NodeAvailabilityWrapper::Active)
788 0 : && matches!(
789 0 : desc.scheduling,
790 0 : NodeSchedulingPolicy::Active | NodeSchedulingPolicy::Filling
791 0 : )
792 0 : });
793 0 :
794 0 : if node_to_fill_descs.is_empty() {
795 0 : anyhow::bail!("There are no nodes to migrate to")
796 0 : }
797 0 :
798 0 : // Set the node scheduling policy to draining for the nodes which
799 0 : // we plan to drain.
800 0 : for node_desc in node_to_drain_descs.iter() {
801 0 : let req = NodeConfigureRequest {
802 0 : node_id: node_desc.id,
803 0 : availability: None,
804 0 : scheduling: Some(NodeSchedulingPolicy::Draining),
805 0 : };
806 0 :
807 0 : storcon_client
808 0 : .dispatch::<_, ()>(
809 0 : Method::PUT,
810 0 : format!("control/v1/node/{}/config", node_desc.id),
811 0 : Some(req),
812 0 : )
813 0 : .await?;
814 0 : }
815 0 :
816 0 : // Perform the migration: move each tenant shard scheduled on a node to
817 0 : // be drained to a node which is being filled. A simple round robin
818 0 : // strategy is used to pick the new node.
819 0 : let tenants = storcon_client
820 0 : .dispatch::<(), Vec<TenantDescribeResponse>>(
821 0 : Method::GET,
822 0 : "control/v1/tenant".to_string(),
823 0 : None,
824 0 : )
825 0 : .await?;
826 0 :
827 0 : let mut selected_node_idx = 0;
828 0 :
829 0 : struct MigrationMove {
830 0 : tenant_shard_id: TenantShardId,
831 0 : from: NodeId,
832 0 : to: NodeId,
833 0 : }
834 0 :
835 0 : let mut moves: Vec<MigrationMove> = Vec::new();
836 0 :
837 0 : let shards = tenants
838 0 : .into_iter()
839 0 : .flat_map(|tenant| tenant.shards.into_iter());
840 0 : for shard in shards {
841 0 : if let Some(max_shards) = max_shards {
842 0 : if moves.len() >= max_shards {
843 0 : println!(
844 0 : "Stop planning shard moves since the requested maximum was reached"
845 0 : );
846 0 : break;
847 0 : }
848 0 : }
849 0 :
850 0 : let should_migrate = {
851 0 : if let Some(attached_to) = shard.node_attached {
852 0 : node_to_drain_descs
853 0 : .iter()
854 0 : .map(|desc| desc.id)
855 0 : .any(|id| id == attached_to)
856 0 : } else {
857 0 : false
858 0 : }
859 0 : };
860 0 :
861 0 : if !should_migrate {
862 0 : continue;
863 0 : }
864 0 :
865 0 : moves.push(MigrationMove {
866 0 : tenant_shard_id: shard.tenant_shard_id,
867 0 : from: shard
868 0 : .node_attached
869 0 : .expect("We only migrate attached tenant shards"),
870 0 : to: node_to_fill_descs[selected_node_idx].id,
871 0 : });
872 0 : selected_node_idx = (selected_node_idx + 1) % node_to_fill_descs.len();
873 0 : }
874 0 :
875 0 : let total_moves = moves.len();
876 0 :
877 0 : if dry_run == Some(true) {
878 0 : println!("Dryrun requested. Planned {total_moves} moves:");
879 0 : for mv in &moves {
880 0 : println!("{}: {} -> {}", mv.tenant_shard_id, mv.from, mv.to)
881 0 : }
882 0 :
883 0 : return Ok(());
884 0 : }
885 0 :
886 0 : const DEFAULT_MIGRATE_CONCURRENCY: usize = 8;
887 0 : let mut stream = futures::stream::iter(moves)
888 0 : .map(|mv| {
889 0 : let client = Client::new(cli.api.clone(), cli.jwt.clone());
890 0 : async move {
891 0 : client
892 0 : .dispatch::<TenantShardMigrateRequest, TenantShardMigrateResponse>(
893 0 : Method::PUT,
894 0 : format!("control/v1/tenant/{}/migrate", mv.tenant_shard_id),
895 0 : Some(TenantShardMigrateRequest {
896 0 : tenant_shard_id: mv.tenant_shard_id,
897 0 : node_id: mv.to,
898 0 : }),
899 0 : )
900 0 : .await
901 0 : .map_err(|e| (mv.tenant_shard_id, mv.from, mv.to, e))
902 0 : }
903 0 : })
904 0 : .buffered(concurrency.unwrap_or(DEFAULT_MIGRATE_CONCURRENCY));
905 0 :
906 0 : let mut success = 0;
907 0 : let mut failure = 0;
908 0 :
909 0 : while let Some(res) = stream.next().await {
910 0 : match res {
911 0 : Ok(_) => {
912 0 : success += 1;
913 0 : }
914 0 : Err((tenant_shard_id, from, to, error)) => {
915 0 : failure += 1;
916 0 : println!(
917 0 : "Failed to migrate {} from node {} to node {}: {}",
918 0 : tenant_shard_id, from, to, error
919 0 : );
920 0 : }
921 0 : }
922 0 :
923 0 : if (success + failure) % 20 == 0 {
924 0 : println!(
925 0 : "Processed {}/{} shards: {} succeeded, {} failed",
926 0 : success + failure,
927 0 : total_moves,
928 0 : success,
929 0 : failure
930 0 : );
931 0 : }
932 0 : }
933 0 :
934 0 : println!(
935 0 : "Processed {}/{} shards: {} succeeded, {} failed",
936 0 : success + failure,
937 0 : total_moves,
938 0 : success,
939 0 : failure
940 0 : );
941 0 : }
942 0 : Command::StartDrain { node_id } => {
943 0 : storcon_client
944 0 : .dispatch::<(), ()>(
945 0 : Method::PUT,
946 0 : format!("control/v1/node/{node_id}/drain"),
947 0 : None,
948 0 : )
949 0 : .await?;
950 0 : println!("Drain started for {node_id}");
951 0 : }
952 0 : Command::CancelDrain { node_id, timeout } => {
953 0 : storcon_client
954 0 : .dispatch::<(), ()>(
955 0 : Method::DELETE,
956 0 : format!("control/v1/node/{node_id}/drain"),
957 0 : None,
958 0 : )
959 0 : .await?;
960 0 :
961 0 : println!("Waiting for node {node_id} to quiesce on scheduling policy ...");
962 0 :
963 0 : let final_policy =
964 0 : wait_for_scheduling_policy(storcon_client, node_id, *timeout, |sched| {
965 0 : use NodeSchedulingPolicy::*;
966 0 : matches!(sched, Active | PauseForRestart)
967 0 : })
968 0 : .await?;
969 0 :
970 0 : println!(
971 0 : "Drain was cancelled for node {node_id}. Schedulling policy is now {final_policy:?}"
972 0 : );
973 0 : }
974 0 : Command::StartFill { node_id } => {
975 0 : storcon_client
976 0 : .dispatch::<(), ()>(Method::PUT, format!("control/v1/node/{node_id}/fill"), None)
977 0 : .await?;
978 0 :
979 0 : println!("Fill started for {node_id}");
980 0 : }
981 0 : Command::CancelFill { node_id, timeout } => {
982 0 : storcon_client
983 0 : .dispatch::<(), ()>(
984 0 : Method::DELETE,
985 0 : format!("control/v1/node/{node_id}/fill"),
986 0 : None,
987 0 : )
988 0 : .await?;
989 0 :
990 0 : println!("Waiting for node {node_id} to quiesce on scheduling policy ...");
991 0 :
992 0 : let final_policy =
993 0 : wait_for_scheduling_policy(storcon_client, node_id, *timeout, |sched| {
994 0 : use NodeSchedulingPolicy::*;
995 0 : matches!(sched, Active)
996 0 : })
997 0 : .await?;
998 0 :
999 0 : println!(
1000 0 : "Fill was cancelled for node {node_id}. Schedulling policy is now {final_policy:?}"
1001 0 : );
1002 0 : }
1003 0 : }
1004 0 :
1005 0 : Ok(())
1006 0 : }
|