1use std::cmp::{self, Ordering, max};
16use std::collections::{HashMap, HashSet, VecDeque};
17use std::ops::Add;
18use std::sync::Arc;
19use std::time::{Duration, SystemTime};
20
21use itertools::Itertools;
22use risingwave_common::hash::WorkerSlotId;
23use risingwave_common::util::addr::HostAddr;
24use risingwave_common::util::resource_util::cpu::total_cpu_available;
25use risingwave_common::util::resource_util::hostname;
26use risingwave_common::util::resource_util::memory::system_memory_available_bytes;
27use risingwave_common::util::version::current_rw_version;
28use risingwave_common::util::worker_util::DEFAULT_RESOURCE_GROUP;
29use risingwave_license::LicenseManager;
30use risingwave_meta_model::prelude::{Worker, WorkerProperty};
31use risingwave_meta_model::worker::{WorkerStatus, WorkerType};
32use risingwave_meta_model::{TransactionId, WorkerId, worker, worker_property};
33use risingwave_pb::common::worker_node::{
34 PbProperty, PbProperty as AddNodeProperty, PbResource, PbState,
35};
36use risingwave_pb::common::{
37 ClusterResource, HostAddress, PbHostAddress, PbWorkerNode, PbWorkerType, WorkerNode,
38};
39use risingwave_pb::meta::subscribe_response::{Info, Operation};
40use sea_orm::ActiveValue::Set;
41use sea_orm::{
42 ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect,
43 TransactionTrait,
44};
45use thiserror_ext::AsReport;
46use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
47use tokio::sync::oneshot::Sender;
48use tokio::sync::{RwLock, RwLockReadGuard};
49use tokio::task::JoinHandle;
50
51use crate::controller::utils::filter_workers_by_resource_group;
52use crate::manager::{LocalNotification, META_NODE_ID, MetaSrvEnv, WorkerKey};
53use crate::model::ClusterId;
54use crate::{MetaError, MetaResult};
55
56pub type ClusterControllerRef = Arc<ClusterController>;
57
58pub struct ClusterController {
59 env: MetaSrvEnv,
60 max_heartbeat_interval: Duration,
61 inner: RwLock<ClusterControllerInner>,
62 started_at: u64,
64}
65
66struct WorkerInfo(worker::Model, Option<worker_property::Model>);
67
68impl From<WorkerInfo> for PbWorkerNode {
69 fn from(info: WorkerInfo) -> Self {
70 Self {
71 id: info.0.worker_id,
72 r#type: PbWorkerType::from(info.0.worker_type) as _,
73 host: Some(PbHostAddress {
74 host: info.0.host,
75 port: info.0.port,
76 }),
77 state: PbState::from(info.0.status) as _,
78 property: info.1.as_ref().map(|p| PbProperty {
79 is_streaming: p.is_streaming,
80 is_serving: p.is_serving,
81 internal_rpc_host_addr: p.internal_rpc_host_addr.clone().unwrap_or_default(),
82 resource_group: p.resource_group.clone(),
83 parallelism: info.1.as_ref().map(|p| p.parallelism).unwrap_or_default() as u32,
84 is_iceberg_compactor: p.is_iceberg_compactor,
85 }),
86 transactional_id: info.0.transaction_id.map(|id| id as _),
87 resource: info
88 .1
89 .as_ref()
90 .and_then(|property| property.resource.as_ref())
91 .map(|resource| resource.to_protobuf()),
92 started_at: info
93 .1
94 .as_ref()
95 .and_then(|property| property.started_at)
96 .map(|started_at| started_at as _),
97 }
98 }
99}
100
101impl ClusterController {
102 pub async fn new(env: MetaSrvEnv, max_heartbeat_interval: Duration) -> MetaResult<Self> {
103 let inner = ClusterControllerInner::new(
104 env.meta_store_ref().conn.clone(),
105 env.opts.disable_automatic_parallelism_control,
106 )
107 .await?;
108 Ok(Self {
109 env,
110 max_heartbeat_interval,
111 inner: RwLock::new(inner),
112 started_at: timestamp_now_sec(),
113 })
114 }
115
116 pub async fn get_inner_read_guard(&self) -> RwLockReadGuard<'_, ClusterControllerInner> {
119 self.inner.read().await
120 }
121
122 pub async fn count_worker_by_type(&self) -> MetaResult<HashMap<WorkerType, i64>> {
123 self.inner.read().await.count_worker_by_type().await
124 }
125
126 pub async fn cluster_resource(&self) -> MetaResult<ClusterResource> {
128 self.inner.read().await.cluster_resource().await
129 }
130
131 async fn update_cluster_resource_for_license(&self) -> MetaResult<()> {
133 let resource = self.cluster_resource().await?;
134
135 LicenseManager::get().update_cluster_resource(resource);
137 self.env.notification_manager().notify_all_without_version(
139 Operation::Update, Info::ClusterResource(resource),
141 );
142
143 Ok(())
144 }
145
146 pub async fn add_worker(
151 &self,
152 r#type: PbWorkerType,
153 host_address: HostAddress,
154 property: AddNodeProperty,
155 resource: PbResource,
156 ) -> MetaResult<WorkerId> {
157 let worker_id = self
158 .inner
159 .write()
160 .await
161 .add_worker(
162 r#type,
163 host_address,
164 property,
165 resource,
166 self.max_heartbeat_interval,
167 )
168 .await?;
169
170 self.update_cluster_resource_for_license().await?;
172
173 Ok(worker_id)
174 }
175
176 pub async fn activate_worker(&self, worker_id: WorkerId) -> MetaResult<()> {
177 let inner = self.inner.write().await;
178 let worker = inner.activate_worker(worker_id).await?;
179
180 if worker.r#type() == PbWorkerType::ComputeNode || worker.r#type() == PbWorkerType::Frontend
183 {
184 self.env
185 .notification_manager()
186 .notify_frontend_without_version(Operation::Add, Info::Node(worker.clone()));
187 }
188 self.env
189 .notification_manager()
190 .notify_local_subscribers(LocalNotification::WorkerNodeActivated(worker));
191
192 Ok(())
193 }
194
195 pub async fn delete_worker(&self, host_address: HostAddress) -> MetaResult<WorkerNode> {
196 let worker = self.inner.write().await.delete_worker(host_address).await?;
197
198 if worker.r#type() == PbWorkerType::ComputeNode || worker.r#type() == PbWorkerType::Frontend
199 {
200 self.env
201 .notification_manager()
202 .notify_frontend_without_version(Operation::Delete, Info::Node(worker.clone()));
203 }
204
205 self.update_cluster_resource_for_license().await?;
207
208 self.env
212 .notification_manager()
213 .notify_local_subscribers(LocalNotification::WorkerNodeDeleted(worker.clone()));
214
215 Ok(worker)
216 }
217
218 pub async fn heartbeat(&self, worker_id: WorkerId) -> MetaResult<()> {
220 tracing::trace!(target: "events::meta::server_heartbeat", %worker_id, "receive heartbeat");
221 self.inner
222 .write()
223 .await
224 .heartbeat(worker_id, self.max_heartbeat_interval)
225 }
226
227 pub fn start_heartbeat_checker(
228 cluster_controller: ClusterControllerRef,
229 check_interval: Duration,
230 ) -> (JoinHandle<()>, Sender<()>) {
231 let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
232 let join_handle = tokio::spawn(async move {
233 let mut min_interval = tokio::time::interval(check_interval);
234 min_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
235 loop {
236 tokio::select! {
237 _ = min_interval.tick() => {},
239 _ = &mut shutdown_rx => {
241 tracing::info!("Heartbeat checker is stopped");
242 return;
243 }
244 }
245
246 let mut inner = cluster_controller.inner.write().await;
247 for worker in inner
249 .worker_extra_info
250 .values_mut()
251 .filter(|worker| worker.expire_at.is_none())
252 {
253 worker.update_ttl(cluster_controller.max_heartbeat_interval);
254 }
255
256 let now = timestamp_now_sec();
258 let worker_to_delete = inner
259 .worker_extra_info
260 .iter()
261 .filter(|(_, info)| info.expire_at.unwrap() < now)
262 .map(|(id, _)| *id)
263 .collect_vec();
264 if worker_to_delete.is_empty() {
265 drop(inner);
266 continue;
267 }
268
269 let worker_infos = match Worker::find()
271 .select_only()
272 .column(worker::Column::WorkerId)
273 .column(worker::Column::WorkerType)
274 .column(worker::Column::Host)
275 .column(worker::Column::Port)
276 .filter(worker::Column::WorkerId.is_in(worker_to_delete.clone()))
277 .into_tuple::<(WorkerId, WorkerType, String, i32)>()
278 .all(&inner.db)
279 .await
280 {
281 Ok(keys) => keys,
282 Err(err) => {
283 tracing::warn!(error = %err.as_report(), "Failed to load expire worker info from db");
284 continue;
285 }
286 };
287 drop(inner);
288
289 for (worker_id, worker_type, host, port) in worker_infos {
290 let host_addr = PbHostAddress { host, port };
291 match cluster_controller.delete_worker(host_addr.clone()).await {
292 Ok(_) => {
293 tracing::warn!(
294 %worker_id,
295 ?host_addr,
296 %now,
297 "Deleted expired worker"
298 );
299 match worker_type {
300 WorkerType::Frontend
301 | WorkerType::ComputeNode
302 | WorkerType::Compactor
303 | WorkerType::RiseCtl => cluster_controller
304 .env
305 .notification_manager()
306 .delete_sender(worker_type.into(), WorkerKey(host_addr)),
307 _ => {}
308 };
309 }
310 Err(err) => {
311 tracing::warn!(error = %err.as_report(), "Failed to delete expire worker from db");
312 }
313 }
314 }
315 }
316 });
317
318 (join_handle, shutdown_tx)
319 }
320
321 pub async fn list_workers(
326 &self,
327 worker_type: Option<WorkerType>,
328 worker_status: Option<WorkerStatus>,
329 ) -> MetaResult<Vec<PbWorkerNode>> {
330 let mut workers = vec![];
331 let include_meta = worker_type.is_none() || worker_type == Some(WorkerType::Meta);
334 let include_meta = include_meta && worker_status != Some(WorkerStatus::Starting);
336 if include_meta {
337 workers.push(meta_node_info(
338 &self.env.opts.advertise_addr,
339 Some(self.started_at),
340 ));
341 }
342 workers.extend(
343 self.inner
344 .read()
345 .await
346 .list_workers(worker_type, worker_status)
347 .await?,
348 );
349 Ok(workers)
350 }
351
352 pub(crate) async fn subscribe_active_streaming_compute_nodes(
353 &self,
354 ) -> MetaResult<(Vec<WorkerNode>, UnboundedReceiver<LocalNotification>)> {
355 let inner = self.inner.read().await;
356 let worker_nodes = inner.list_active_streaming_workers().await?;
357 let (tx, rx) = unbounded_channel();
358
359 self.env.notification_manager().insert_local_sender(tx);
361 drop(inner);
362 Ok((worker_nodes, rx))
363 }
364
365 pub async fn list_active_streaming_workers(&self) -> MetaResult<Vec<PbWorkerNode>> {
368 self.inner
369 .read()
370 .await
371 .list_active_streaming_workers()
372 .await
373 }
374
375 pub async fn list_active_worker_slots(&self) -> MetaResult<Vec<WorkerSlotId>> {
376 self.inner.read().await.list_active_worker_slots().await
377 }
378
379 pub async fn list_active_serving_workers(&self) -> MetaResult<Vec<PbWorkerNode>> {
381 self.inner.read().await.list_active_serving_workers().await
382 }
383
384 pub async fn get_streaming_cluster_info(&self) -> MetaResult<StreamingClusterInfo> {
386 self.inner.read().await.get_streaming_cluster_info().await
387 }
388
389 pub async fn get_worker_by_id(&self, worker_id: WorkerId) -> MetaResult<Option<PbWorkerNode>> {
390 self.inner.read().await.get_worker_by_id(worker_id).await
391 }
392
393 pub async fn get_worker_info_by_id(&self, worker_id: WorkerId) -> Option<WorkerExtraInfo> {
394 self.inner
395 .read()
396 .await
397 .get_worker_extra_info_by_id(worker_id)
398 }
399
400 pub fn cluster_id(&self) -> &ClusterId {
401 self.env.cluster_id()
402 }
403
404 pub fn meta_store_endpoint(&self) -> String {
405 self.env.meta_store_ref().endpoint.clone()
406 }
407}
408
409#[derive(Debug, Clone)]
411pub struct StreamingClusterInfo {
412 pub worker_nodes: HashMap<WorkerId, WorkerNode>,
414}
415
416impl StreamingClusterInfo {
418 pub fn parallelism(&self, resource_group: &str) -> usize {
419 let available_worker_ids =
420 filter_workers_by_resource_group(&self.worker_nodes, resource_group);
421
422 self.worker_nodes
423 .values()
424 .filter(|worker| available_worker_ids.contains(&(worker.id)))
425 .map(|worker| worker.compute_node_parallelism())
426 .sum()
427 }
428
429 pub fn filter_workers_by_resource_group(
430 &self,
431 resource_group: &str,
432 ) -> HashMap<WorkerId, WorkerNode> {
433 let worker_ids = filter_workers_by_resource_group(&self.worker_nodes, resource_group);
434 self.worker_nodes
435 .iter()
436 .filter(|(id, _)| worker_ids.contains(*id))
437 .map(|(id, worker)| (*id, worker.clone()))
438 .collect()
439 }
440}
441
442#[derive(Default, Clone, Debug)]
443pub struct WorkerExtraInfo {
444 expire_at: Option<u64>,
446}
447
448impl WorkerExtraInfo {
449 fn update_ttl(&mut self, ttl: Duration) {
450 let expire = cmp::max(
451 self.expire_at.unwrap_or_default(),
452 SystemTime::now()
453 .add(ttl)
454 .duration_since(SystemTime::UNIX_EPOCH)
455 .expect("Clock may have gone backwards")
456 .as_secs(),
457 );
458 self.expire_at = Some(expire);
459 }
460}
461
462fn timestamp_now_sec() -> u64 {
463 SystemTime::now()
464 .duration_since(SystemTime::UNIX_EPOCH)
465 .expect("Clock may have gone backwards")
466 .as_secs()
467}
468
469fn meta_node_info(host: &str, started_at: Option<u64>) -> PbWorkerNode {
470 PbWorkerNode {
471 id: META_NODE_ID,
472 r#type: PbWorkerType::Meta.into(),
473 host: HostAddr::try_from(host)
474 .as_ref()
475 .map(HostAddr::to_protobuf)
476 .ok(),
477 state: PbState::Running as _,
478 property: None,
479 transactional_id: None,
480 resource: Some(risingwave_pb::common::worker_node::Resource {
481 rw_version: current_rw_version(),
482 total_memory_bytes: system_memory_available_bytes() as _,
483 total_cpu_cores: total_cpu_available() as _,
484 hostname: hostname(),
485 }),
486 started_at,
487 }
488}
489
490pub struct ClusterControllerInner {
491 db: DatabaseConnection,
492 available_transactional_ids: VecDeque<TransactionId>,
494 worker_extra_info: HashMap<WorkerId, WorkerExtraInfo>,
495 disable_automatic_parallelism_control: bool,
496}
497
498impl ClusterControllerInner {
499 pub const MAX_WORKER_REUSABLE_ID_BITS: usize = 10;
500 pub const MAX_WORKER_REUSABLE_ID_COUNT: usize = 1 << Self::MAX_WORKER_REUSABLE_ID_BITS;
501
502 pub async fn new(
503 db: DatabaseConnection,
504 disable_automatic_parallelism_control: bool,
505 ) -> MetaResult<Self> {
506 let workers = Worker::find()
507 .select_only()
508 .column(worker::Column::WorkerId)
509 .column(worker::Column::TransactionId)
510 .into_tuple::<(WorkerId, Option<TransactionId>)>()
511 .all(&db)
512 .await?;
513 let inuse_txn_ids: HashSet<_> = workers
514 .iter()
515 .filter_map(|(_, transaction_id)| *transaction_id)
516 .collect();
517 let available_transactional_ids = (0..Self::MAX_WORKER_REUSABLE_ID_COUNT as TransactionId)
518 .filter(|id| !inuse_txn_ids.contains(id))
519 .collect();
520
521 let worker_extra_info = workers
522 .into_iter()
523 .map(|(worker_id, _)| (worker_id, WorkerExtraInfo::default()))
524 .collect();
525
526 Ok(Self {
527 db,
528 available_transactional_ids,
529 worker_extra_info,
530 disable_automatic_parallelism_control,
531 })
532 }
533
534 pub async fn count_worker_by_type(&self) -> MetaResult<HashMap<WorkerType, i64>> {
535 let workers: Vec<(WorkerType, i64)> = Worker::find()
536 .select_only()
537 .column(worker::Column::WorkerType)
538 .column_as(worker::Column::WorkerId.count(), "count")
539 .group_by(worker::Column::WorkerType)
540 .into_tuple()
541 .all(&self.db)
542 .await?;
543
544 Ok(workers.into_iter().collect())
545 }
546
547 pub fn update_worker_ttl(&mut self, worker_id: WorkerId, ttl: Duration) -> MetaResult<()> {
548 if let Some(info) = self.worker_extra_info.get_mut(&worker_id) {
549 let expire = cmp::max(
550 info.expire_at.unwrap_or_default(),
551 SystemTime::now()
552 .add(ttl)
553 .duration_since(SystemTime::UNIX_EPOCH)
554 .expect("Clock may have gone backwards")
555 .as_secs(),
556 );
557 info.expire_at = Some(expire);
558 Ok(())
559 } else {
560 Err(MetaError::invalid_worker(worker_id, "worker not found"))
561 }
562 }
563
564 fn apply_transaction_id(&self, r#type: PbWorkerType) -> MetaResult<Option<TransactionId>> {
565 match (self.available_transactional_ids.front(), r#type) {
566 (None, _) => Err(MetaError::unavailable("no available reusable machine id")),
567 (Some(id), PbWorkerType::ComputeNode | PbWorkerType::Frontend) => Ok(Some(*id)),
569 _ => Ok(None),
570 }
571 }
572
573 async fn cluster_resource(&self) -> MetaResult<ClusterResource> {
575 let mut per_host = HashMap::new();
577
578 per_host.insert(
581 hostname(),
582 ClusterResource {
583 total_cpu_cores: total_cpu_available() as _,
584 total_memory_bytes: system_memory_available_bytes() as _,
585 },
586 );
587
588 let worker_properties = WorkerProperty::find().all(&self.db).await?;
589 for resource in worker_properties
590 .into_iter()
591 .filter_map(|property| property.resource)
592 .map(|resource| resource.to_protobuf())
593 {
594 let r = per_host
595 .entry(resource.hostname.clone())
596 .or_insert_with(ClusterResource::default);
597
598 r.total_cpu_cores = max(r.total_cpu_cores, resource.total_cpu_cores);
599 r.total_memory_bytes = max(r.total_memory_bytes, resource.total_memory_bytes);
600 }
601
602 Ok(per_host
604 .into_values()
605 .reduce(|a, b| ClusterResource {
606 total_cpu_cores: a.total_cpu_cores + b.total_cpu_cores,
607 total_memory_bytes: a.total_memory_bytes + b.total_memory_bytes,
608 })
609 .unwrap_or_default())
610 }
611
612 #[await_tree::instrument]
613 pub async fn add_worker(
614 &mut self,
615 r#type: PbWorkerType,
616 host_address: HostAddress,
617 add_property: AddNodeProperty,
618 resource: PbResource,
619 ttl: Duration,
620 ) -> MetaResult<WorkerId> {
621 let txn = self.db.begin().await?;
622
623 let worker = Worker::find()
624 .filter(
625 worker::Column::Host
626 .eq(host_address.host.clone())
627 .and(worker::Column::Port.eq(host_address.port)),
628 )
629 .find_also_related(WorkerProperty)
630 .one(&txn)
631 .await?;
632 if let Some((worker, property)) = worker {
634 assert_eq!(worker.worker_type, r#type.into());
635 let started_at = timestamp_now_sec();
636 return if worker.worker_type == WorkerType::ComputeNode {
637 let property = property.unwrap();
638 let mut current_parallelism = property.parallelism as usize;
639 let new_parallelism = add_property.parallelism as usize;
640 match new_parallelism.cmp(¤t_parallelism) {
641 Ordering::Less => {
642 if !self.disable_automatic_parallelism_control {
643 tracing::info!(
645 "worker {} parallelism reduced from {} to {}",
646 worker.worker_id,
647 current_parallelism,
648 new_parallelism
649 );
650 current_parallelism = new_parallelism;
651 } else {
652 tracing::warn!(
655 "worker {} parallelism is less than current, current is {}, but received {}",
656 worker.worker_id,
657 current_parallelism,
658 new_parallelism
659 );
660 }
661 }
662 Ordering::Greater => {
663 tracing::info!(
664 "worker {} parallelism updated from {} to {}",
665 worker.worker_id,
666 current_parallelism,
667 new_parallelism
668 );
669 current_parallelism = new_parallelism;
670 }
671 Ordering::Equal => {}
672 }
673 let mut property: worker_property::ActiveModel = property.into();
674
675 property.is_streaming = Set(add_property.is_streaming);
676 property.is_serving = Set(add_property.is_serving);
677 property.parallelism = Set(current_parallelism as _);
678 property.resource = Set(Some((&resource).into()));
679 property.started_at = Set(Some(started_at as _));
680 property.resource_group =
681 Set(Some(add_property.resource_group.unwrap_or_else(|| {
682 tracing::warn!(
683 "resource_group is not set for worker {}, fallback to `default`",
684 worker.worker_id
685 );
686 DEFAULT_RESOURCE_GROUP.to_owned()
687 })));
688
689 WorkerProperty::update(property).exec(&txn).await?;
690 txn.commit().await?;
691 self.update_worker_ttl(worker.worker_id, ttl)?;
692 Ok(worker.worker_id)
693 } else if worker.worker_type == WorkerType::Frontend && property.is_none() {
694 let worker_property = worker_property::ActiveModel {
695 worker_id: Set(worker.worker_id),
696 parallelism: Set(add_property
697 .parallelism
698 .try_into()
699 .expect("invalid parallelism")),
700 is_streaming: Set(add_property.is_streaming),
701 is_serving: Set(add_property.is_serving),
702 is_unschedulable: Set(false),
703 internal_rpc_host_addr: Set(Some(add_property.internal_rpc_host_addr)),
704 resource_group: Set(None),
705 is_iceberg_compactor: Set(false),
706 resource: Set(Some((&resource).into())),
707 started_at: Set(Some(started_at as _)),
708 };
709 WorkerProperty::insert(worker_property).exec(&txn).await?;
710 txn.commit().await?;
711 self.update_worker_ttl(worker.worker_id, ttl)?;
712 Ok(worker.worker_id)
713 } else if worker.worker_type == WorkerType::Compactor {
714 if let Some(property) = property {
715 let mut property: worker_property::ActiveModel = property.into();
716 property.is_iceberg_compactor = Set(add_property.is_iceberg_compactor);
717 property.internal_rpc_host_addr =
718 Set(Some(add_property.internal_rpc_host_addr));
719 property.resource = Set(Some((&resource).into()));
720 property.started_at = Set(Some(started_at as _));
721
722 WorkerProperty::update(property).exec(&txn).await?;
723 } else {
724 let property = worker_property::ActiveModel {
725 worker_id: Set(worker.worker_id),
726 parallelism: Set(add_property
727 .parallelism
728 .try_into()
729 .expect("invalid parallelism")),
730 is_streaming: Set(false),
731 is_serving: Set(false),
732 is_unschedulable: Set(false),
733 internal_rpc_host_addr: Set(Some(add_property.internal_rpc_host_addr)),
734 resource_group: Set(None),
735 is_iceberg_compactor: Set(add_property.is_iceberg_compactor),
736 resource: Set(Some((&resource).into())),
737 started_at: Set(Some(started_at as _)),
738 };
739
740 WorkerProperty::insert(property).exec(&txn).await?;
741 }
742 txn.commit().await?;
743 self.update_worker_ttl(worker.worker_id, ttl)?;
744 Ok(worker.worker_id)
745 } else {
746 if let Some(property) = property {
747 let mut property: worker_property::ActiveModel = property.into();
748 property.resource = Set(Some((&resource).into()));
749 property.started_at = Set(Some(started_at as _));
750 WorkerProperty::update(property).exec(&txn).await?;
751 txn.commit().await?;
752 }
753 self.update_worker_ttl(worker.worker_id, ttl)?;
754 Ok(worker.worker_id)
755 };
756 }
757
758 let txn_id = self.apply_transaction_id(r#type)?;
759
760 let worker = worker::ActiveModel {
761 worker_id: Default::default(),
762 worker_type: Set(r#type.into()),
763 host: Set(host_address.host.clone()),
764 port: Set(host_address.port),
765 status: Set(WorkerStatus::Starting),
766 transaction_id: Set(txn_id),
767 };
768 let insert_res = Worker::insert(worker).exec(&txn).await?;
769 let worker_id = insert_res.last_insert_id as WorkerId;
770 let started_at = timestamp_now_sec();
771 if r#type == PbWorkerType::ComputeNode
772 || r#type == PbWorkerType::Frontend
773 || r#type == PbWorkerType::Compactor
774 {
775 let (is_serving, is_streaming, is_iceberg_compactor, resource_group) = match r#type {
776 PbWorkerType::ComputeNode => (
777 add_property.is_serving,
778 add_property.is_streaming,
779 false,
780 add_property.resource_group.clone(),
781 ),
782 PbWorkerType::Frontend => (
783 add_property.is_serving,
784 add_property.is_streaming,
785 false,
786 None,
787 ),
788 PbWorkerType::Compactor => (false, false, add_property.is_iceberg_compactor, None),
789 _ => unreachable!(),
790 };
791
792 let property = worker_property::ActiveModel {
793 worker_id: Set(worker_id),
794 parallelism: Set(add_property
795 .parallelism
796 .try_into()
797 .expect("invalid parallelism")),
798 is_streaming: Set(is_streaming),
799 is_serving: Set(is_serving),
800 is_unschedulable: Set(false),
801 internal_rpc_host_addr: Set(Some(add_property.internal_rpc_host_addr)),
802 resource_group: Set(resource_group),
803 is_iceberg_compactor: Set(is_iceberg_compactor),
804 resource: Set(Some((&resource).into())),
805 started_at: Set(Some(started_at as _)),
806 };
807 WorkerProperty::insert(property).exec(&txn).await?;
808 }
809
810 txn.commit().await?;
811 if let Some(txn_id) = txn_id {
812 self.available_transactional_ids.retain(|id| *id != txn_id);
813 }
814 let extra_info = WorkerExtraInfo { expire_at: None };
815 self.worker_extra_info.insert(worker_id, extra_info);
816
817 Ok(worker_id)
818 }
819
820 pub async fn activate_worker(&self, worker_id: WorkerId) -> MetaResult<PbWorkerNode> {
821 let worker = worker::ActiveModel {
822 worker_id: Set(worker_id),
823 status: Set(WorkerStatus::Running),
824 ..Default::default()
825 };
826
827 let worker = worker.update(&self.db).await?;
828 let worker_property = WorkerProperty::find_by_id(worker.worker_id)
829 .one(&self.db)
830 .await?;
831 Ok(WorkerInfo(worker, worker_property).into())
832 }
833
834 pub async fn delete_worker(&mut self, host_addr: HostAddress) -> MetaResult<PbWorkerNode> {
835 let worker = Worker::find()
836 .filter(
837 worker::Column::Host
838 .eq(host_addr.host)
839 .and(worker::Column::Port.eq(host_addr.port)),
840 )
841 .find_also_related(WorkerProperty)
842 .one(&self.db)
843 .await?;
844 let Some((worker, property)) = worker else {
845 return Err(MetaError::invalid_parameter("worker not found!"));
846 };
847
848 let res = Worker::delete_by_id(worker.worker_id)
849 .exec(&self.db)
850 .await?;
851 if res.rows_affected == 0 {
852 return Err(MetaError::invalid_parameter("worker not found!"));
853 }
854
855 self.worker_extra_info.remove(&worker.worker_id).unwrap();
856 if let Some(txn_id) = &worker.transaction_id {
857 self.available_transactional_ids.push_back(*txn_id);
858 }
859 let worker: PbWorkerNode = WorkerInfo(worker, property).into();
860
861 Ok(worker)
862 }
863
864 pub fn heartbeat(&mut self, worker_id: WorkerId, ttl: Duration) -> MetaResult<()> {
865 let Some(worker_info) = self.worker_extra_info.get_mut(&worker_id) else {
866 return Err(MetaError::invalid_worker(worker_id, "worker not found"));
867 };
868
869 worker_info.update_ttl(ttl);
870 Ok(())
871 }
872
873 pub async fn list_workers(
874 &self,
875 worker_type: Option<WorkerType>,
876 worker_status: Option<WorkerStatus>,
877 ) -> MetaResult<Vec<PbWorkerNode>> {
878 let mut find = Worker::find();
879 if let Some(worker_type) = worker_type {
880 find = find.filter(worker::Column::WorkerType.eq(worker_type));
881 }
882 if let Some(worker_status) = worker_status {
883 find = find.filter(worker::Column::Status.eq(worker_status));
884 }
885 let workers = find.find_also_related(WorkerProperty).all(&self.db).await?;
886 Ok(workers
887 .into_iter()
888 .map(|(worker, property)| WorkerInfo(worker, property).into())
889 .collect_vec())
890 }
891
892 pub async fn list_active_streaming_workers(&self) -> MetaResult<Vec<PbWorkerNode>> {
893 let workers = Worker::find()
894 .filter(
895 worker::Column::WorkerType
896 .eq(WorkerType::ComputeNode)
897 .and(worker::Column::Status.eq(WorkerStatus::Running)),
898 )
899 .inner_join(WorkerProperty)
900 .select_also(WorkerProperty)
901 .filter(worker_property::Column::IsStreaming.eq(true))
902 .all(&self.db)
903 .await?;
904
905 Ok(workers
906 .into_iter()
907 .map(|(worker, property)| WorkerInfo(worker, property).into())
908 .collect_vec())
909 }
910
911 pub async fn list_active_worker_slots(&self) -> MetaResult<Vec<WorkerSlotId>> {
912 let worker_parallelisms: Vec<(WorkerId, i32)> = WorkerProperty::find()
913 .select_only()
914 .column(worker_property::Column::WorkerId)
915 .column(worker_property::Column::Parallelism)
916 .inner_join(Worker)
917 .filter(worker::Column::Status.eq(WorkerStatus::Running))
918 .into_tuple()
919 .all(&self.db)
920 .await?;
921 Ok(worker_parallelisms
922 .into_iter()
923 .flat_map(|(worker_id, parallelism)| {
924 (0..parallelism).map(move |idx| WorkerSlotId::new(worker_id, idx as usize))
925 })
926 .collect_vec())
927 }
928
929 pub async fn list_active_serving_workers(&self) -> MetaResult<Vec<PbWorkerNode>> {
930 let workers = Worker::find()
931 .filter(
932 worker::Column::WorkerType
933 .eq(WorkerType::ComputeNode)
934 .and(worker::Column::Status.eq(WorkerStatus::Running)),
935 )
936 .inner_join(WorkerProperty)
937 .select_also(WorkerProperty)
938 .filter(worker_property::Column::IsServing.eq(true))
939 .all(&self.db)
940 .await?;
941
942 Ok(workers
943 .into_iter()
944 .map(|(worker, property)| WorkerInfo(worker, property).into())
945 .collect_vec())
946 }
947
948 pub async fn get_streaming_cluster_info(&self) -> MetaResult<StreamingClusterInfo> {
949 let streaming_workers = self.list_active_streaming_workers().await?;
950
951 let active_workers: HashMap<_, _> =
952 streaming_workers.into_iter().map(|w| (w.id, w)).collect();
953
954 Ok(StreamingClusterInfo {
955 worker_nodes: active_workers,
956 })
957 }
958
959 pub async fn get_worker_by_id(&self, worker_id: WorkerId) -> MetaResult<Option<PbWorkerNode>> {
960 let worker = Worker::find_by_id(worker_id)
961 .find_also_related(WorkerProperty)
962 .one(&self.db)
963 .await?;
964 if worker.is_none() {
965 return Ok(None);
966 }
967 Ok(worker.map(|(w, p)| WorkerInfo(w, p).into()))
968 }
969
970 pub fn get_worker_extra_info_by_id(&self, worker_id: WorkerId) -> Option<WorkerExtraInfo> {
971 self.worker_extra_info.get(&worker_id).cloned()
972 }
973}
974
975#[cfg(test)]
976mod tests {
977 use super::*;
978
979 fn mock_worker_hosts_for_test(count: usize) -> Vec<HostAddress> {
980 (0..count)
981 .map(|i| HostAddress {
982 host: "localhost".to_owned(),
983 port: 5000 + i as i32,
984 })
985 .collect_vec()
986 }
987
988 #[tokio::test]
989 async fn test_cluster_controller() -> MetaResult<()> {
990 let env = MetaSrvEnv::for_test().await;
991 let cluster_ctl = ClusterController::new(env, Duration::from_secs(1)).await?;
992
993 let parallelism_num = 4_usize;
994 let worker_count = 5_usize;
995 let property = AddNodeProperty {
996 parallelism: parallelism_num as _,
997 is_streaming: true,
998 is_serving: true,
999 ..Default::default()
1000 };
1001 let hosts = mock_worker_hosts_for_test(worker_count);
1002 let mut worker_ids = vec![];
1003 for host in &hosts {
1004 worker_ids.push(
1005 cluster_ctl
1006 .add_worker(
1007 PbWorkerType::ComputeNode,
1008 host.clone(),
1009 property.clone(),
1010 PbResource::default(),
1011 )
1012 .await?,
1013 );
1014 }
1015
1016 assert_eq!(cluster_ctl.list_active_worker_slots().await?.len(), 0);
1018
1019 for id in &worker_ids {
1020 cluster_ctl.activate_worker(*id).await?;
1021 }
1022 let worker_cnt_map = cluster_ctl.count_worker_by_type().await?;
1023 assert_eq!(
1024 *worker_cnt_map.get(&WorkerType::ComputeNode).unwrap() as usize,
1025 worker_count
1026 );
1027 assert_eq!(
1028 cluster_ctl.list_active_streaming_workers().await?.len(),
1029 worker_count
1030 );
1031 assert_eq!(
1032 cluster_ctl.list_active_serving_workers().await?.len(),
1033 worker_count
1034 );
1035 assert_eq!(
1036 cluster_ctl.list_active_worker_slots().await?.len(),
1037 parallelism_num * worker_count
1038 );
1039
1040 let mut new_property = property.clone();
1042 new_property.parallelism = (parallelism_num * 2) as _;
1043 new_property.is_serving = false;
1044 cluster_ctl
1045 .add_worker(
1046 PbWorkerType::ComputeNode,
1047 hosts[0].clone(),
1048 new_property,
1049 PbResource::default(),
1050 )
1051 .await?;
1052
1053 assert_eq!(
1054 cluster_ctl.list_active_streaming_workers().await?.len(),
1055 worker_count
1056 );
1057 assert_eq!(
1058 cluster_ctl.list_active_serving_workers().await?.len(),
1059 worker_count - 1
1060 );
1061 let worker_slots = cluster_ctl.list_active_worker_slots().await?;
1062 assert!(worker_slots.iter().all_unique());
1063 assert_eq!(worker_slots.len(), parallelism_num * (worker_count + 1));
1064
1065 for host in hosts {
1067 cluster_ctl.delete_worker(host).await?;
1068 }
1069 assert_eq!(cluster_ctl.list_active_streaming_workers().await?.len(), 0);
1070 assert_eq!(cluster_ctl.list_active_serving_workers().await?.len(), 0);
1071 assert_eq!(cluster_ctl.list_active_worker_slots().await?.len(), 0);
1072
1073 Ok(())
1074 }
1075
1076 #[tokio::test]
1077 async fn test_list_workers_include_meta_node() -> MetaResult<()> {
1078 let env = MetaSrvEnv::for_test().await;
1079 let cluster_ctl = ClusterController::new(env, Duration::from_secs(1)).await?;
1080
1081 let workers = cluster_ctl.list_workers(None, None).await?;
1083 assert!(workers.iter().any(|w| w.r#type() == PbWorkerType::Meta));
1084
1085 let workers = cluster_ctl
1087 .list_workers(Some(WorkerType::Meta), None)
1088 .await?;
1089 assert_eq!(workers.len(), 1);
1090 assert_eq!(workers[0].r#type(), PbWorkerType::Meta);
1091
1092 let workers = cluster_ctl
1094 .list_workers(Some(WorkerType::Meta), Some(WorkerStatus::Starting))
1095 .await?;
1096 assert!(workers.is_empty());
1097
1098 Ok(())
1099 }
1100
1101 #[tokio::test]
1102 async fn test_heartbeat_does_not_update_resource() -> MetaResult<()> {
1103 let env = MetaSrvEnv::for_test().await;
1104 let cluster_ctl = ClusterController::new(env.clone(), Duration::from_secs(1)).await?;
1105
1106 let host = HostAddress {
1107 host: "localhost".to_owned(),
1108 port: 5010,
1109 };
1110 let property = AddNodeProperty {
1111 is_streaming: true,
1112 is_serving: true,
1113 parallelism: 4,
1114 ..Default::default()
1115 };
1116
1117 let resource_v1 = PbResource {
1118 rw_version: "rw-v1".to_owned(),
1119 total_memory_bytes: 1024,
1120 total_cpu_cores: 4,
1121 hostname: "host-v1".to_owned(),
1122 };
1123 let worker_id = cluster_ctl
1124 .add_worker(
1125 PbWorkerType::ComputeNode,
1126 host.clone(),
1127 property,
1128 resource_v1.clone(),
1129 )
1130 .await?;
1131
1132 cluster_ctl.heartbeat(worker_id).await?;
1133
1134 let worker = cluster_ctl
1135 .get_worker_by_id(worker_id)
1136 .await?
1137 .expect("worker should exist");
1138 assert_eq!(
1139 worker.resource.expect("worker resource should exist"),
1140 resource_v1
1141 );
1142
1143 let recovered_cluster_ctl = ClusterController::new(env, Duration::from_secs(1)).await?;
1144 let worker = recovered_cluster_ctl
1145 .get_worker_by_id(worker_id)
1146 .await?
1147 .expect("worker should exist");
1148 assert_eq!(
1149 worker.resource.expect("worker resource should be restored"),
1150 resource_v1
1151 );
1152
1153 recovered_cluster_ctl.delete_worker(host).await?;
1154 Ok(())
1155 }
1156
1157 #[tokio::test]
1158 async fn test_cluster_controller_restores_worker_extra_info() -> MetaResult<()> {
1159 let env = MetaSrvEnv::for_test().await;
1160 let cluster_ctl = ClusterController::new(env.clone(), Duration::from_secs(1)).await?;
1161
1162 let host = HostAddress {
1163 host: "localhost".to_owned(),
1164 port: 5012,
1165 };
1166 let property = AddNodeProperty {
1167 is_streaming: true,
1168 is_serving: true,
1169 parallelism: 4,
1170 ..Default::default()
1171 };
1172 let resource = PbResource {
1173 rw_version: "rw-v1".to_owned(),
1174 total_memory_bytes: 1024,
1175 total_cpu_cores: 4,
1176 hostname: "host-v1".to_owned(),
1177 };
1178 let worker_id = cluster_ctl
1179 .add_worker(
1180 PbWorkerType::ComputeNode,
1181 host.clone(),
1182 property,
1183 resource.clone(),
1184 )
1185 .await?;
1186 let started_at = cluster_ctl
1187 .get_worker_by_id(worker_id)
1188 .await?
1189 .expect("worker should exist")
1190 .started_at;
1191
1192 let recovered_cluster_ctl = ClusterController::new(env, Duration::from_secs(1)).await?;
1193 let recovered_worker = recovered_cluster_ctl
1194 .get_worker_by_id(worker_id)
1195 .await?
1196 .expect("worker should exist after recovery");
1197 assert_eq!(
1198 recovered_worker
1199 .resource
1200 .expect("worker resource should be restored"),
1201 resource
1202 );
1203 assert_eq!(recovered_worker.started_at, started_at);
1204
1205 recovered_cluster_ctl.delete_worker(host).await?;
1206 Ok(())
1207 }
1208
1209 #[tokio::test]
1210 async fn test_reregister_compute_node_updates_resource() -> MetaResult<()> {
1211 let env = MetaSrvEnv::for_test().await;
1212 let cluster_ctl = ClusterController::new(env, Duration::from_secs(1)).await?;
1213
1214 let host = HostAddress {
1215 host: "localhost".to_owned(),
1216 port: 5011,
1217 };
1218 let property = AddNodeProperty {
1219 is_streaming: true,
1220 is_serving: true,
1221 parallelism: 4,
1222 ..Default::default()
1223 };
1224
1225 let resource_v1 = PbResource {
1226 rw_version: "rw-v1".to_owned(),
1227 total_memory_bytes: 1024,
1228 total_cpu_cores: 4,
1229 hostname: "host-v1".to_owned(),
1230 };
1231 let worker_id = cluster_ctl
1232 .add_worker(
1233 PbWorkerType::ComputeNode,
1234 host.clone(),
1235 property.clone(),
1236 resource_v1,
1237 )
1238 .await?;
1239
1240 let resource_v2 = PbResource {
1241 rw_version: "rw-v2".to_owned(),
1242 total_memory_bytes: 2048,
1243 total_cpu_cores: 8,
1244 hostname: "host-v2".to_owned(),
1245 };
1246 cluster_ctl
1247 .add_worker(
1248 PbWorkerType::ComputeNode,
1249 host.clone(),
1250 property,
1251 resource_v2.clone(),
1252 )
1253 .await?;
1254
1255 let worker = cluster_ctl
1256 .get_worker_by_id(worker_id)
1257 .await?
1258 .expect("worker should exist");
1259 assert_eq!(
1260 worker.resource.expect("worker resource should exist"),
1261 resource_v2
1262 );
1263
1264 cluster_ctl.delete_worker(host).await?;
1265 Ok(())
1266 }
1267}