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