Skip to main content

risingwave_meta/controller/
cluster.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    /// Used as timestamp when meta node starts in sec.
63    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    /// Used in `NotificationService::subscribe`.
117    /// Need to pay attention to the order of acquiring locks to prevent deadlock problems.
118    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    /// Get the total resource of the cluster.
127    pub async fn cluster_resource(&self) -> MetaResult<ClusterResource> {
128        self.inner.read().await.cluster_resource().await
129    }
130
131    /// Get the total resource of the cluster, then update license manager and notify all other nodes.
132    async fn update_cluster_resource_for_license(&self) -> MetaResult<()> {
133        let resource = self.cluster_resource().await?;
134
135        // Update local license manager.
136        LicenseManager::get().update_cluster_resource(resource);
137        // Notify all other nodes.
138        self.env.notification_manager().notify_all_without_version(
139            Operation::Update, // unused
140            Info::ClusterResource(resource),
141        );
142
143        Ok(())
144    }
145
146    /// A worker node will immediately register itself to meta when it bootstraps.
147    /// The meta will assign it with a unique ID and set its state as `Starting`.
148    /// When the worker node is fully ready to serve, it will request meta again
149    /// (via `activate_worker_node`) to set its state to `Running`.
150    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        // Keep license manager in sync with the latest cluster resource.
171        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        // Notify frontends of new compute node and frontend node.
181        // Always notify because a running worker's property may have been changed.
182        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        // Keep license manager in sync with the latest cluster resource.
206        self.update_cluster_resource_for_license().await?;
207
208        // Notify local subscribers.
209        // Note: Any type of workers may pin some hummock resource. So `HummockManager` expect this
210        // local notification.
211        self.env
212            .notification_manager()
213            .notify_local_subscribers(LocalNotification::WorkerNodeDeleted(worker.clone()));
214
215        Ok(worker)
216    }
217
218    /// Invoked when it receives a heartbeat from a worker node.
219    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                    // Wait for interval
238                    _ = min_interval.tick() => {},
239                    // Shutdown
240                    _ = &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                // 1. Initialize new workers' TTL.
248                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                // 2. Collect expired workers.
257                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                // 3. Delete expired workers.
270                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    /// Get live nodes with the specified type and state.
322    /// # Arguments
323    /// * `worker_type` `WorkerType` of the nodes
324    /// * `worker_state` Filter by this state if it is not None.
325    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        // Meta node is not stored in the cluster manager DB, so we synthesize it here.
332        // Include it when listing all workers, or when explicitly listing meta nodes.
333        let include_meta = worker_type.is_none() || worker_type == Some(WorkerType::Meta);
334        // Meta node is always "running" once the service is up.
335        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        // insert before release the read lock to ensure that we don't lose any update in between
360        self.env.notification_manager().insert_local_sender(tx);
361        drop(inner);
362        Ok((worker_nodes, rx))
363    }
364
365    /// A convenient method to get all running compute nodes that may have running actors on them
366    /// i.e. CNs which are running
367    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    /// Get the cluster info used for scheduling a streaming job, containing all active serving nodes.
380    pub async fn list_active_serving_workers(&self) -> MetaResult<Vec<PbWorkerNode>> {
381        self.inner.read().await.list_active_serving_workers().await
382    }
383
384    /// Get the cluster info used for scheduling a streaming job.
385    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/// The cluster info used for scheduling a streaming job.
410#[derive(Debug, Clone)]
411pub struct StreamingClusterInfo {
412    /// All **active** compute nodes in the cluster.
413    pub worker_nodes: HashMap<WorkerId, WorkerNode>,
414}
415
416// Encapsulating the use of parallelism
417impl 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    // Unix timestamp that the worker will expire at.
445    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    /// Record for tracking available machine ids, one is available.
493    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            // We only assign transactional id to compute node and frontend.
568            (Some(id), PbWorkerType::ComputeNode | PbWorkerType::Frontend) => Ok(Some(*id)),
569            _ => Ok(None),
570        }
571    }
572
573    /// Get the total resource of the cluster.
574    async fn cluster_resource(&self) -> MetaResult<ClusterResource> {
575        // For each hostname, we only consider the maximum resource, in case a host has multiple nodes.
576        let mut per_host = HashMap::new();
577
578        // Note: Meta node itself is not a "worker" and thus won't register via `add_worker_node`.
579        // Still, for license/RWU enforcement we should include the resources used by the meta node.
580        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        // For different hostnames, we sum up the resources.
603        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        // Worker already exist.
633        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(&current_parallelism) {
641                    Ordering::Less => {
642                        if !self.disable_automatic_parallelism_control {
643                            // Handing over to the subsequent recovery loop for a forced reschedule.
644                            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                            // Warn and keep the original parallelism if the worker registered with a
653                            // smaller parallelism.
654                            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        // Since no worker is active, the parallelism should be 0.
1017        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        // re-register existing worker node with larger parallelism and change its serving mode.
1041        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        // delete workers.
1066        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        // List all workers should include the synthesized meta node.
1082        let workers = cluster_ctl.list_workers(None, None).await?;
1083        assert!(workers.iter().any(|w| w.r#type() == PbWorkerType::Meta));
1084
1085        // Explicitly listing meta workers should also include it.
1086        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        // Listing starting workers should not include meta.
1093        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}