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(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        // Keep license manager in sync with the latest cluster resource.
208        self.update_cluster_resource_for_license().await?;
209
210        // Notify local subscribers.
211        // Note: Any type of workers may pin some hummock resource. So `HummockManager` expect this
212        // local notification.
213        self.env
214            .notification_manager()
215            .notify_local_subscribers(LocalNotification::WorkerNodeDeleted(worker.clone()));
216
217        Ok(worker)
218    }
219
220    /// Invoked when it receives a heartbeat from a worker node.
221    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                    // Wait for interval
240                    _ = min_interval.tick() => {},
241                    // Shutdown
242                    _ = &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                // 1. Initialize new workers' TTL.
250                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                // 2. Collect expired workers.
259                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                // 3. Delete expired workers.
272                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    /// Get live nodes with the specified type and state.
324    /// # Arguments
325    /// * `worker_type` `WorkerType` of the nodes
326    /// * `worker_state` Filter by this state if it is not None.
327    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        // Meta node is not stored in the cluster manager DB, so we synthesize it here.
334        // Include it when listing all workers, or when explicitly listing meta nodes.
335        let include_meta = worker_type.is_none() || worker_type == Some(WorkerType::Meta);
336        // Meta node is always "running" once the service is up.
337        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        // insert before release the read lock to ensure that we don't lose any update in between
362        self.env.notification_manager().insert_local_sender(tx);
363        drop(inner);
364        Ok((worker_nodes, rx))
365    }
366
367    /// A convenient method to get all running compute nodes that may have running actors on them
368    /// i.e. CNs which are running
369    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    /// Get the cluster info used for scheduling a streaming job, containing all active serving nodes.
382    pub async fn list_active_serving_workers(&self) -> MetaResult<Vec<PbWorkerNode>> {
383        self.inner.read().await.list_active_serving_workers().await
384    }
385
386    /// Get the cluster info used for scheduling a streaming job.
387    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/// The cluster info used for scheduling a streaming job.
412#[derive(Debug, Clone)]
413pub struct StreamingClusterInfo {
414    /// All **active** compute nodes in the cluster.
415    pub worker_nodes: HashMap<WorkerId, WorkerNode>,
416}
417
418// Encapsulating the use of parallelism
419impl 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    // Unix timestamp that the worker will expire at.
447    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    /// Record for tracking available machine ids, one is available.
495    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            // We only assign transactional id to compute node and frontend.
570            (Some(id), PbWorkerType::ComputeNode | PbWorkerType::Frontend) => Ok(Some(*id)),
571            _ => Ok(None),
572        }
573    }
574
575    /// Get the total resource of the cluster.
576    async fn cluster_resource(&self) -> MetaResult<ClusterResource> {
577        // For each hostname, we only consider the maximum resource, in case a host has multiple nodes.
578        let mut per_host = HashMap::new();
579
580        // Note: Meta node itself is not a "worker" and thus won't register via `add_worker_node`.
581        // Still, for license/RWU enforcement we should include the resources used by the meta node.
582        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        // For different hostnames, we sum up the resources.
605        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        // Worker already exist.
635        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(&current_parallelism) {
643                    Ordering::Less => {
644                        if !self.disable_automatic_parallelism_control {
645                            // Handing over to the subsequent recovery loop for a forced reschedule.
646                            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                            // Warn and keep the original parallelism if the worker registered with a
655                            // smaller parallelism.
656                            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        // Since no worker is active, the parallelism should be 0.
1019        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        // re-register existing worker node with larger parallelism and change its serving mode.
1043        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        // delete workers.
1068        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        // List all workers should include the synthesized meta node.
1084        let workers = cluster_ctl.list_workers(None, None).await?;
1085        assert!(workers.iter().any(|w| w.r#type() == PbWorkerType::Meta));
1086
1087        // Explicitly listing meta workers should also include it.
1088        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        // Listing starting workers should not include meta.
1095        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}