Repository: reearth/reearth-flow
Pull request #1119: chore(websocket): improve Redis connection handling in broadcast updates [FLOW-BE-107]
Author: login: kasugamirai
Labels: name: websocket
Created: 2025-03-25T14:27:47Z | Merged: 2025-03-27T06:27:12Z

# Overview

## What I've done

## What I haven't done

## How I tested

## Screenshot

## Which point I want you to review particularly

## Memo


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced HTTP compression for improved response handling in the server.
  - Added new modules for tasks and types, expanding the functionality of the broadcast system.
  - Implemented a `BackgroundTasks` struct for managing multiple asynchronous tasks related to the websocket server.
  
- **Refactor**
  - Enhanced the handling of Redis connections for more direct interaction during message broadcasting.
  - Streamlined the subscription creation process for improved clarity and conciseness.
  - Simplified the retrieval of document names and IDs, reducing unnecessary conditional checks.
  - Updated methods to require non-optional parameters, improving clarity and efficiency.
  - Modified the command execution process in Redis to utilize a Lua script for better performance.

- **Chores**
  - Added new dependencies for Tower and Tower HTTP to enhance HTTP request handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/server/Cargo.lock b/server/Cargo.lock
index aad1e8e772..2784b035f7 100644
--- a/server/Cargo.lock
+++ b/server/Cargo.lock
@@ -26,6 +26,21 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
 [[package]]
 name = "android-tzdata"
 version = "0.1.1"
@@ -65,6 +80,19 @@ version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
 
+[[package]]
+name = "async-compression"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
+dependencies = [
+ "brotli",
+ "futures-core",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+]
+
 [[package]]
 name = "async-lock"
 version = "3.4.0"
@@ -235,6 +263,27 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "brotli"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
 [[package]]
 name = "bumpalo"
 version = "3.17.0"
@@ -1918,9 +1967,9 @@ checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
 
 [[package]]
 name = "rustls-webpki"
-version = "0.103.0"
+version = "0.103.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f"
+checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
 dependencies = [
  "ring",
  "rustls-pki-types",
@@ -2416,6 +2465,25 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "tower-http"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
+dependencies = [
+ "async-compression",
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+]
+
 [[package]]
 name = "tower-layer"
 version = "0.3.3"
@@ -2757,6 +2825,8 @@ dependencies = [
  "tokio",
  "tokio-tungstenite",
  "tokio-util",
+ "tower",
+ "tower-http",
  "tracing",
  "tracing-subscriber",
  "url",
diff --git a/server/websocket/Cargo.toml b/server/websocket/Cargo.toml
index ffc7f30aea..e05a38cf5f 100644
--- a/server/websocket/Cargo.toml
+++ b/server/websocket/Cargo.toml
@@ -56,6 +56,8 @@ base64 = "0.22.1"
 deadpool = "0.12.2"
 deadpool-redis = "0.20.0"
 bytes = "1.10.1"
+tower = "0.5.2"
+tower-http = { version = "0.6.2", features = ["compression-br"] }
 
 [dev-dependencies]
 tokio-tungstenite = "0.26.2"
diff --git a/server/websocket/src/broadcast/group.rs b/server/websocket/src/broadcast/group.rs
index c02329ac8c..e4ff49b38a 100644
--- a/server/websocket/src/broadcast/group.rs
+++ b/server/websocket/src/broadcast/group.rs
@@ -1,3 +1,4 @@
+use crate::broadcast::tasks::BackgroundTasks;
 use crate::storage::gcs::GcsStore;
 use crate::storage::kv::DocOps;
 use crate::storage::redis::RedisStore;
@@ -5,6 +6,7 @@ use crate::AwarenessRef;
 
 use anyhow::Result;
 use bytes::Bytes;
+use deadpool_redis::Connection;
 use futures_util::{SinkExt, StreamExt};
 use rand;
 
@@ -24,27 +26,21 @@ use yrs::updates::decoder::Decode;
 use yrs::updates::encoder::{Encode, Encoder, EncoderV1};
 use yrs::{Doc, ReadTxn, Transact, Update};
 
-#[derive(Debug, Clone)]
-pub struct BroadcastConfig {
-    pub storage_enabled: bool,
-    pub doc_name: Option<String>,
-}
+use super::types::BroadcastConfig;
 
 pub struct BroadcastGroup {
     connections: Arc<AtomicUsize>,
     awareness_ref: AwarenessRef,
     sender: Sender<Bytes>,
-    awareness_updater: JoinHandle<()>,
+    background_tasks: Arc<Mutex<BackgroundTasks>>,
     doc_sub: Option<yrs::Subscription>,
     awareness_sub: Option<yrs::Subscription>,
-    storage: Option<Arc<GcsStore>>,
+    storage: Arc<GcsStore>,
     redis_store: Arc<RedisStore>,
-    doc_name: Option<String>,
-    redis_subscriber_task: Option<JoinHandle<()>>,
+    doc_name: String,
     redis_consumer_name: Option<String>,
     redis_group_name: Option<String>,
     shutdown_complete: AtomicBool,
-    heartbeat_task: Option<JoinHandle<()>>,
     instance_id: String,
 }
 
@@ -93,6 +89,8 @@ impl BroadcastGroup {
         awareness: AwarenessRef,
         buffer_capacity: usize,
         redis_store: Arc<RedisStore>,
+        storage: Arc<GcsStore>,
+        doc_name: String,
     ) -> Result<Self> {
         let (sender, _receiver) = channel(buffer_capacity);
         let awareness_c = Arc::downgrade(&awareness);
@@ -115,6 +113,8 @@ impl BroadcastGroup {
         let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
         let sink = sender.clone();
 
+        let (awareness_shutdown_tx, mut awareness_shutdown_rx) = tokio::sync::mpsc::channel(1);
+
         let awareness_sub = lock.on_update(move |_awareness, event, _origin| {
             let added = event.added();
             let updated = event.updated();
@@ -136,38 +136,58 @@ impl BroadcastGroup {
         drop(lock);
 
         let awareness_updater = tokio::task::spawn(async move {
-            while let Some(changed_clients) = rx.recv().await {
-                if let Some(awareness) = awareness_c.upgrade() {
-                    let awareness = awareness.read().await;
-                    if let Ok(update) = awareness.update_with_clients(changed_clients) {
-                        let msg_bytes = Bytes::from(Message::Awareness(update).encode_v1());
-                        if sink.send(msg_bytes).is_err() {
-                            tracing::warn!("couldn't broadcast awareness update");
+            loop {
+                select! {
+                    _ = awareness_shutdown_rx.recv() => {
+                        tracing::debug!("Awareness updater received shutdown signal");
+                        break;
+                    },
+                    client_update = rx.recv() => {
+                        match client_update {
+                            Some(changed_clients) => {
+                                if let Some(awareness) = awareness_c.upgrade() {
+                                    let awareness = awareness.read().await;
+                                    if let Ok(update) = awareness.update_with_clients(changed_clients) {
+                                        let msg_bytes = Bytes::from(Message::Awareness(update).encode_v1());
+                                        if sink.send(msg_bytes).is_err() {
+                                            tracing::warn!("couldn't broadcast awareness update");
+                                        }
+                                    }
+                                } else {
+                                    break;
+                                }
+                            },
+                            None => {
+                                tracing::debug!("Awareness update channel closed");
+                                break;
+                            }
                         }
                     }
-                } else {
-                    return;
                 }
             }
+            tracing::debug!("Awareness updater task exited gracefully");
         });
 
         let instance_id = format!("instance-{}", rand::random::<u64>());
 
+        let background_tasks = Arc::new(Mutex::new(BackgroundTasks::new(
+            awareness_updater,
+            awareness_shutdown_tx,
+        )));
+
         let result = Self {
             connections: Arc::new(AtomicUsize::new(0)),
             awareness_ref: awareness,
             sender,
-            awareness_updater,
+            background_tasks,
             doc_sub: Some(doc_sub),
             awareness_sub: Some(awareness_sub),
-            storage: None,
+            storage,
             redis_store,
-            doc_name: None,
-            redis_subscriber_task: None,
+            doc_name,
             redis_consumer_name: None,
             redis_group_name: None,
             shutdown_complete: AtomicBool::new(false),
-            heartbeat_task: None,
             instance_id,
         };
 
@@ -181,13 +201,19 @@ impl BroadcastGroup {
         redis_store: Arc<RedisStore>,
         config: BroadcastConfig,
     ) -> Result<Self> {
+        let doc_name = config.doc_name.unwrap_or_default();
         if !config.storage_enabled {
-            return Self::new(awareness, buffer_capacity, redis_store.clone()).await;
+            return Self::new(awareness, buffer_capacity, redis_store, store, doc_name).await;
         }
 
-        let mut group = Self::new(awareness, buffer_capacity, redis_store.clone()).await?;
-
-        let doc_name = config.doc_name.clone().unwrap_or_default();
+        let mut group = Self::new(
+            awareness,
+            buffer_capacity,
+            redis_store.clone(),
+            store.clone(),
+            doc_name.clone(),
+        )
+        .await?;
 
         Self::load_from_storage(&store, &doc_name, &group.awareness_ref).await;
 
@@ -201,6 +227,8 @@ impl BroadcastGroup {
         let group_name = format!("yjs-group-{}", consumer_name);
         let group_name_clone = group_name.clone();
 
+        let (redis_shutdown_tx, mut redis_shutdown_rx) = tokio::sync::mpsc::channel(1);
+
         let redis_subscriber_task = tokio::spawn(async move {
             if let Err(e) = redis_store_for_sub
                 .create_consumer_group(&doc_name_for_sub, &group_name_clone)
@@ -224,97 +252,104 @@ impl BroadcastGroup {
             };
 
             loop {
-                match redis_store_for_sub
-                    .read_and_ack(
-                        &mut conn,
-                        &stream_key,
-                        &group_name_clone,
-                        &consumer_name_clone,
-                        15,
-                    )
-                    .await
-                {
-                    Ok(updates) => {
-                        consecutive_errors = 0;
-                        if !updates.is_empty() {
-                            let awareness = awareness_for_sub.write().await;
-                            let mut txn = awareness.doc().transact_mut();
-
-                            for update in updates {
-                                let decode_result = Update::decode_v1(&update);
-
-                                if let Ok(decoded) = decode_result {
-                                    if let Err(e) = txn.apply_update(decoded) {
-                                        tracing::warn!("Failed to apply update from Redis: {}", e);
+                select! {
+                    _ = redis_shutdown_rx.recv() => {
+                        tracing::debug!("Redis subscriber received shutdown signal");
+                        break;
+                    },
+                    _ = async {
+                        match redis_store_for_sub
+                            .read_and_ack(
+                                &mut conn,
+                                &stream_key,
+                                &group_name_clone,
+                                &consumer_name_clone,
+                                16,
+                            )
+                            .await
+                        {
+                            Ok(updates) => {
+                                consecutive_errors = 0;
+                                if !updates.is_empty() {
+                                    let awareness = awareness_for_sub.write().await;
+                                    let mut txn = awareness.doc().transact_mut();
+
+                                    for update in updates {
+                                        let decode_result = Update::decode_v1(&update);
+
+                                        if let Ok(decoded) = decode_result {
+                                            if let Err(e) = txn.apply_update(decoded) {
+                                                tracing::warn!("Failed to apply update from Redis: {}", e);
+                                            }
+                                        }
+
+                                        if sender_for_sub.send(update).is_err() {
+                                            tracing::debug!("Failed to broadcast Redis update");
+                                        }
                                     }
                                 }
-
-                                if sender_for_sub.send(update).is_err() {
-                                    tracing::debug!("Failed to broadcast Redis update");
-                                }
                             }
-                        }
-                    }
-                    Err(e) => {
-                        tracing::error!("Error reading from Redis Stream: {}", e);
+                            Err(e) => {
+                                tracing::error!("Error reading from Redis Stream: {}", e);
+
+                                consecutive_errors += 1;
+                                total_errors += 1;
+                                if consecutive_errors >= max_consecutive_errors
+                                    || total_errors >= max_total_errors
+                                {
+                                    tracing::warn!(
+                                        "Too many Redis errors ({} total, {} consecutive), stopping subscriber",
+                                        total_errors, consecutive_errors
+                                    );
+                                    return;
+                                }
 
-                        consecutive_errors += 1;
-                        total_errors += 1;
-                        if consecutive_errors >= max_consecutive_errors
-                            || total_errors >= max_total_errors
-                        {
-                            tracing::warn!(
-                                "Too many Redis errors ({} total, {} consecutive), stopping subscriber",
-                                total_errors, consecutive_errors
-                            );
-                            return;
+                                tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
+                            }
                         }
 
-                        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
-                    }
+                        tokio::task::yield_now().await;
+                    } => {}
                 }
-
-                tokio::task::yield_now().await;
             }
+            tracing::debug!("Redis subscriber task exited gracefully");
         });
 
-        group.redis_subscriber_task = Some(redis_subscriber_task);
+        {
+            let mut background_tasks = group.background_tasks.lock().await;
+            background_tasks.set_redis_subscriber(redis_subscriber_task, redis_shutdown_tx);
+        }
         group.redis_consumer_name = Some(consumer_name);
         group.redis_group_name = Some(group_name);
 
-        group.storage = Some(store);
-        group.doc_name = Some(doc_name.clone());
-
-        let redis_store_clone = redis_store.clone();
-        let doc_name_clone = doc_name.clone();
-        let shutdown_flag = Arc::new(AtomicBool::new(false));
-        let shutdown_flag_clone = shutdown_flag.clone();
+        let (heartbeat_shutdown_tx, mut heartbeat_shutdown_rx) = tokio::sync::mpsc::channel(1);
         let instance_id = group.instance_id.clone();
 
         let heartbeat_task = tokio::spawn(async move {
             let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(56));
 
-            while !shutdown_flag_clone.load(Ordering::Relaxed) {
-                interval.tick().await;
-
-                if let Err(e) = redis_store_clone
-                    .update_instance_heartbeat(&doc_name_clone, &instance_id)
-                    .await
-                {
-                    tracing::warn!("Failed to update instance heartbeat: {}", e);
-                } else {
-                    tracing::debug!(
-                        "Updated heartbeat for doc '{}' instance {}",
-                        doc_name_clone,
-                        instance_id
-                    );
+            loop {
+                select! {
+                    _ = heartbeat_shutdown_rx.recv() => {
+                        tracing::debug!("Heartbeat task received shutdown signal");
+                        break;
+                    },
+                    _ = interval.tick() => {
+                        if let Err(e) = redis_store
+                            .update_instance_heartbeat(&doc_name, &instance_id)
+                            .await
+                        {
+                            tracing::warn!("Failed to update instance heartbeat: {}", e);
+                        }
+                    }
                 }
             }
-
-            tracing::debug!("Heartbeat task for '{}' stopped", doc_name_clone);
         });
 
-        group.heartbeat_task = Some(heartbeat_task);
+        {
+            let mut background_tasks = group.background_tasks.lock().await;
+            background_tasks.set_heartbeat(heartbeat_task, heartbeat_shutdown_tx);
+        }
 
         Ok(group)
     }
@@ -338,15 +373,15 @@ impl BroadcastGroup {
         &self.awareness_ref
     }
 
-    pub fn get_redis_store(&self) -> Arc<RedisStore> {
-        self.redis_store.clone()
+    pub fn get_redis_store(&self) -> &Arc<RedisStore> {
+        &self.redis_store
     }
 
-    pub fn get_redis_group_name(&self) -> Option<String> {
-        self.redis_group_name.clone()
+    pub fn get_redis_group_name(&self) -> Option<&String> {
+        self.redis_group_name.as_ref()
     }
 
-    pub fn get_doc_name(&self) -> Option<String> {
+    pub fn get_doc_name(&self) -> String {
         self.doc_name.clone()
     }
 
@@ -367,12 +402,11 @@ impl BroadcastGroup {
         <Sink as futures_util::Sink<Bytes>>::Error: std::error::Error + Send + Sync,
         E: std::error::Error + Send + Sync + 'static,
     {
-        let doc_id = self.doc_name.clone().unwrap_or_default();
         let current_count = self.connection_count();
 
-        tracing::info!(
+        tracing::debug!(
             "Creating new subscription for doc '{}', current count: {}",
-            doc_id,
+            self.doc_name,
             current_count
         );
 
@@ -447,9 +481,9 @@ impl BroadcastGroup {
             let awareness = self.awareness().clone();
             let redis_store = self.redis_store.clone();
             let doc_name = self.doc_name.clone();
-            let stream_key = format!("yjs:stream:{}", doc_name.clone().unwrap_or_default());
-
+            let stream_key = format!("yjs:stream:{}", doc_name);
             tokio::spawn(async move {
+                let mut conn = redis_store.get_pool().get().await.unwrap();
                 while let Some(res) = stream.next().await {
                     let data = match res.map_err(anyhow::Error::from) {
                         Ok(data) => data,
@@ -472,8 +506,8 @@ impl BroadcastGroup {
                         &awareness,
                         msg,
                         &redis_store,
-                        doc_name.as_ref(),
-                        stream_key.clone(),
+                        &stream_key,
+                        &mut conn,
                     )
                     .await
                     {
@@ -503,25 +537,23 @@ impl BroadcastGroup {
         awareness: &AwarenessRef,
         msg: Message,
         redis_store: &Arc<RedisStore>,
-        doc_name: Option<&String>,
-        stream_key: String,
+        stream_key: &str,
+        conn: &mut Connection,
     ) -> Result<Option<Message>, Error> {
         match msg {
             Message::Sync(msg) => {
-                if doc_name.is_some() {
-                    let rs = redis_store.clone();
-                    let update_bytes = match &msg {
-                        SyncMessage::Update(update) => update.clone(),
-                        SyncMessage::SyncStep2(update) => update.clone(),
-                        _ => Vec::new(),
-                    };
+                let update_bytes = match &msg {
+                    SyncMessage::Update(update) => update.clone(),
+                    SyncMessage::SyncStep2(update) => update.clone(),
+                    _ => Vec::new(),
+                };
 
-                    if !update_bytes.is_empty() {
-                        tokio::spawn(async move {
-                            if let Err(e) = rs.publish_update(&stream_key, &update_bytes).await {
-                                tracing::error!("Redis Stream update failed: {}", e);
-                            }
-                        });
+                if !update_bytes.is_empty() {
+                    if let Err(e) = redis_store
+                        .publish_update(stream_key, &update_bytes, conn)
+                        .await
+                    {
+                        tracing::error!("Redis Stream update failed: {}", e);
                     }
                 }
 
@@ -567,153 +599,139 @@ impl BroadcastGroup {
             .store(true, std::sync::atomic::Ordering::SeqCst);
 
         if self.connection_count() == 0 {
-            if let (Some(store), Some(doc_name)) = (&self.storage, &self.doc_name) {
-                let store_clone = store.clone();
-                let doc_name_clone = doc_name.clone();
-                let awareness = self.awareness_ref.clone();
-                let redis_store_clone = self.redis_store.clone();
-
-                if let Err(e) = redis_store_clone
-                    .remove_instance_heartbeat(&doc_name_clone, &self.instance_id)
-                    .await
-                {
-                    tracing::warn!(
-                        "Failed to remove instance heartbeat before checking connections: {}",
-                        e
-                    );
-                } else {
-                    tracing::debug!(
-                        "Removed heartbeat for instance {} before checking connections",
-                        self.instance_id
-                    );
+            let redis_store_clone = self.redis_store.clone();
+            let store_clone = self.storage.clone();
+
+            if let Err(e) = redis_store_clone
+                .remove_instance_heartbeat(&self.doc_name, &self.instance_id)
+                .await
+            {
+                tracing::warn!(
+                    "Failed to remove instance heartbeat before checking connections: {}",
+                    e
+                );
+            } else {
+                tracing::debug!(
+                    "Removed heartbeat for instance {} before checking connections",
+                    self.instance_id
+                );
+            }
+
+            let should_save = match redis_store_clone
+                .get_active_instances(&self.doc_name, 60)
+                .await
+            {
+                Ok(connections) => {
+                    if connections <= 0 {
+                        tracing::debug!(
+                            "All instances disconnected from '{}', proceeding with GCS save",
+                            self.doc_name
+                        );
+                        true
+                    } else {
+                        tracing::debug!(
+                            "Other instances still connected to '{}', skipping GCS save",
+                            self.doc_name
+                        );
+                        false
+                    }
+                }
+                Err(e) => {
+                    tracing::warn!("Failed to get Redis connection count: {}", e);
+                    true
                 }
+            };
+
+            if should_save {
+                let lock_id = format!("gcs:lock:{}", self.doc_name);
+                let instance_id = format!("instance-{}", rand::random::<u64>());
 
-                let should_save = match redis_store_clone
-                    .get_active_instances(&doc_name_clone, 60)
+                let lock_acquired = match redis_store_clone
+                    .acquire_doc_lock(&lock_id, &instance_id)
                     .await
                 {
-                    Ok(connections) => {
-                        if connections <= 0 {
-                            tracing::info!(
-                                "All instances disconnected from '{}', proceeding with GCS save",
-                                doc_name_clone
-                            );
-                            true
-                        } else {
-                            tracing::debug!(
-                                "Other instances still connected to '{}', skipping GCS save",
-                                doc_name_clone
-                            );
-                            false
-                        }
+                    Ok(true) => {
+                        tracing::debug!("Acquired lock for GCS operations on {}", self.doc_name);
+                        Some((redis_store_clone.clone(), lock_id, instance_id))
+                    }
+                    Ok(false) => {
+                        tracing::warn!(
+                            "Could not acquire lock for GCS operations, skipping update"
+                        );
+                        None
                     }
                     Err(e) => {
-                        tracing::warn!("Failed to get Redis connection count: {}", e);
-                        true
+                        tracing::warn!("Error acquiring lock for GCS operations: {}", e);
+                        None
                     }
                 };
 
-                if should_save {
-                    let lock_id = format!("gcs:lock:{}", doc_name_clone);
-                    let instance_id = format!("instance-{}", rand::random::<u64>());
-
-                    let lock_acquired = match redis_store_clone
-                        .acquire_doc_lock(&lock_id, &instance_id)
-                        .await
-                    {
-                        Ok(true) => {
-                            tracing::debug!(
-                                "Acquired lock for GCS operations on {}",
-                                doc_name_clone
-                            );
-                            Some((redis_store_clone.clone(), lock_id, instance_id))
-                        }
-                        Ok(false) => {
-                            tracing::warn!(
-                                "Could not acquire lock for GCS operations, skipping update"
-                            );
-                            None
-                        }
-                        Err(e) => {
-                            tracing::warn!("Error acquiring lock for GCS operations: {}", e);
-                            None
-                        }
-                    };
-
-                    if lock_acquired.is_some() {
-                        let awareness = awareness.write().await;
-                        let awareness_doc = awareness.doc();
-                        let _awareness_state = awareness_doc.transact().state_vector();
+                if lock_acquired.is_some() {
+                    let awareness = self.awareness_ref.write().await;
+                    let awareness_doc = awareness.doc();
+                    let _awareness_state = awareness_doc.transact().state_vector();
 
-                        let gcs_doc = Doc::new();
-                        let mut gcs_txn = gcs_doc.transact_mut();
+                    let gcs_doc = Doc::new();
+                    let mut gcs_txn = gcs_doc.transact_mut();
 
-                        if let Err(e) = store_clone.load_doc(&doc_name_clone, &mut gcs_txn).await {
-                            tracing::warn!("Failed to load current state from GCS: {}", e);
-                        }
+                    if let Err(e) = store_clone.load_doc(&self.doc_name, &mut gcs_txn).await {
+                        tracing::warn!("Failed to load current state from GCS: {}", e);
+                    }
 
-                        let gcs_state = gcs_txn.state_vector();
+                    let gcs_state = gcs_txn.state_vector();
 
-                        let awareness_txn = awareness_doc.transact();
-                        let update = awareness_txn.encode_diff_v1(&gcs_state);
-                        let update_bytes = Bytes::from(update);
-                        Self::handle_gcs_update(update_bytes, &doc_name_clone, &store_clone).await;
-                    }
+                    let awareness_txn = awareness_doc.transact();
+                    let update = awareness_txn.encode_diff_v1(&gcs_state);
+                    let update_bytes = Bytes::from(update);
+                    Self::handle_gcs_update(update_bytes, &self.doc_name, &store_clone).await;
+                }
 
-                    if let Some((redis, lock_id, instance_id)) = lock_acquired {
-                        if let Err(e) = redis.release_doc_lock(&lock_id, &instance_id).await {
-                            tracing::warn!("Failed to release GCS lock: {}", e);
-                        }
+                if let Some((redis, lock_id, instance_id)) = lock_acquired {
+                    if let Err(e) = redis.release_doc_lock(&lock_id, &instance_id).await {
+                        tracing::warn!("Failed to release GCS lock: {}", e);
                     }
                 }
             }
         }
 
-        if let Some(task) = &self.redis_subscriber_task {
-            task.abort();
-        }
-
-        if let Some(task) = &self.heartbeat_task {
-            task.abort();
+        {
+            let mut background_tasks = self.background_tasks.lock().await;
+            background_tasks.stop_all();
         }
 
-        self.awareness_updater.abort();
+        let redis_store_clone = self.redis_store.clone();
+        let doc_name_clone = self.doc_name.clone();
+        let instance_id = self.instance_id.clone();
 
-        if let Some(doc_name) = &self.doc_name {
-            let redis_store_clone = self.redis_store.clone();
-            let doc_name_clone = doc_name.clone();
-            let instance_id = self.instance_id.clone();
-
-            tokio::spawn(async move {
-                match redis_store_clone
-                    .safe_delete_stream(&doc_name_clone, &instance_id)
-                    .await
-                {
-                    Ok(deleted) => {
-                        if deleted {
-                            tracing::info!(
-                                "Successfully deleted Redis stream for '{}'",
-                                doc_name_clone
-                            );
-                        } else {
-                            tracing::info!(
-                                "Did not delete Redis stream for '{}' as it may still be in use",
-                                doc_name_clone
-                            );
-                        }
-                    }
-                    Err(e) => {
-                        tracing::warn!(
-                            "Error during safe Redis stream deletion for '{}': {}",
-                            doc_name_clone,
-                            e
+        tokio::spawn(async move {
+            match redis_store_clone
+                .safe_delete_stream(&doc_name_clone, &instance_id)
+                .await
+            {
+                Ok(deleted) => {
+                    if deleted {
+                        tracing::debug!(
+                            "Successfully deleted Redis stream for '{}'",
+                            doc_name_clone
+                        );
+                    } else {
+                        tracing::debug!(
+                            "Did not delete Redis stream for '{}' as it may still be in use",
+                            doc_name_clone
                         );
                     }
                 }
+                Err(e) => {
+                    tracing::warn!(
+                        "Error during safe Redis stream deletion for '{}': {}",
+                        doc_name_clone,
+                        e
+                    );
+                }
+            }
 
-                tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
-            });
-        }
+            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
+        });
 
         Ok(())
     }
@@ -729,18 +747,14 @@ impl Drop for BroadcastGroup {
             drop(sub);
         }
 
-        if let Some(task) = self.redis_subscriber_task.take() {
-            task.abort();
-        }
-
-        if let Some(task) = self.heartbeat_task.take() {
-            task.abort();
-        }
-
-        self.awareness_updater.abort();
-
         self.shutdown_complete
             .store(true, std::sync::atomic::Ordering::SeqCst);
+
+        let background_tasks = self.background_tasks.clone();
+        tokio::spawn(async move {
+            let mut tasks = background_tasks.lock().await;
+            tasks.stop_all();
+        });
     }
 }
 
diff --git a/server/websocket/src/broadcast/mod.rs b/server/websocket/src/broadcast/mod.rs
index ba49e461f2..6cca0fd715 100644
--- a/server/websocket/src/broadcast/mod.rs
+++ b/server/websocket/src/broadcast/mod.rs
@@ -1,2 +1,4 @@
 pub mod group;
 pub mod pool;
+pub mod tasks;
+pub mod types;
diff --git a/server/websocket/src/broadcast/pool.rs b/server/websocket/src/broadcast/pool.rs
index 40ca3c73c9..e2cb2d293d 100644
--- a/server/websocket/src/broadcast/pool.rs
+++ b/server/websocket/src/broadcast/pool.rs
@@ -1,4 +1,4 @@
-use crate::broadcast::group::{BroadcastConfig, BroadcastGroup};
+use crate::broadcast::group::BroadcastGroup;
 use crate::storage::gcs::GcsStore;
 use crate::storage::kv::DocOps;
 use crate::storage::redis::RedisStore;
@@ -14,12 +14,9 @@ use yrs::sync::Awareness;
 use yrs::updates::decoder::Decode;
 use yrs::{Doc, ReadTxn, StateVector, Transact, Update};
 
-const DEFAULT_DOC_ID: &str = "01jpjfpw0qtw17kbrcdbgefakg";
+use super::types::{BroadcastConfig, BroadcastGroupContext};
 
-#[derive(Debug)]
-pub struct BroadcastGroupContext {
-    group: Arc<BroadcastGroup>,
-}
+const DEFAULT_DOC_ID: &str = "01jpjfpw0qtw17kbrcdbgefakg";
 
 #[derive(Debug, Clone)]
 pub struct BroadcastGroupManager {
@@ -47,22 +44,19 @@ impl BroadcastGroupManager {
                 let group_clone = entry.get().clone();
                 drop(entry);
 
-                if let Some(doc_name) = group_clone.get_doc_name() {
-                    let redis_store = group_clone.get_redis_store();
-                    let valid = match redis_store.check_stream_exists(&doc_name).await {
-                        Ok(exists) => exists,
-                        Err(e) => {
-                            tracing::warn!("Error checking Redis stream: {}", e);
-                            false
-                        }
-                    };
-
-                    if !valid {
-                        tracing::warn!("Found cached broadcast group for '{}' but Redis stream does not exist, recreating", doc_id);
-                        self.doc_to_id_map.remove(&doc_id_string);
-                    } else {
-                        return Ok(group_clone);
+                let doc_name = group_clone.get_doc_name();
+                let redis_store = group_clone.get_redis_store();
+                let valid = match redis_store.check_stream_exists(&doc_name).await {
+                    Ok(exists) => exists,
+                    Err(e) => {
+                        tracing::warn!("Error checking Redis stream: {}", e);
+                        false
                     }
+                };
+
+                if !valid {
+                    tracing::warn!("Found cached broadcast group for '{}' but Redis stream does not exist, recreating", doc_id);
+                    self.doc_to_id_map.remove(&doc_id_string);
                 } else {
                     return Ok(group_clone);
                 }
@@ -199,7 +193,7 @@ impl Manager for BroadcastGroupManager {
 
         async move {
             if group.connection_count() == 0 {
-                let doc_id = group.get_doc_name().unwrap_or_default();
+                let doc_id = group.get_doc_name();
                 tracing::info!("Recycling empty broadcast group for document '{}'", doc_id);
 
                 doc_to_id_map.remove(&doc_id);
@@ -215,7 +209,7 @@ impl Manager for BroadcastGroupManager {
                     "Group has no connections".into(),
                 ));
             }
-            let doc_id = group.get_doc_name().unwrap_or_default();
+            let doc_id = group.get_doc_name();
             tracing::info!("Recycling broadcast group for document '{}'", doc_id);
             Ok(())
         }
@@ -229,7 +223,7 @@ pub struct BroadcastPool {
 
 impl BroadcastPool {
     pub fn new(store: Arc<GcsStore>, redis_store: Arc<RedisStore>) -> Self {
-        let manager = BroadcastGroupManager::new(store.clone(), redis_store.clone());
+        let manager = BroadcastGroupManager::new(store, redis_store);
 
         let doc_to_id_map = manager.doc_to_id_map.clone();
         tokio::spawn(async move {
@@ -291,7 +285,7 @@ impl BroadcastPool {
 
         if let Some(group) = broadcast_group {
             let store = self.get_store();
-            let doc_name = group.get_doc_name().unwrap_or_else(|| doc_id.to_string());
+            let doc_name = group.get_doc_name();
 
             let redis_store = group.get_redis_store();
             let active_connections = match redis_store.get_active_instances(&doc_name, 60).await {
diff --git a/server/websocket/src/broadcast/tasks.rs b/server/websocket/src/broadcast/tasks.rs
new file mode 100644
index 0000000000..fdd0ccf21e
--- /dev/null
+++ b/server/websocket/src/broadcast/tasks.rs
@@ -0,0 +1,83 @@
+use tokio::task::JoinHandle;
+
+pub struct BackgroundTasks {
+    awareness_updater: JoinHandle<()>,
+    awareness_shutdown_tx: tokio::sync::mpsc::Sender<()>,
+    redis_subscriber_task: Option<JoinHandle<()>>,
+    redis_subscriber_shutdown_tx: Option<tokio::sync::mpsc::Sender<()>>,
+    heartbeat_task: Option<JoinHandle<()>>,
+    heartbeat_shutdown_tx: Option<tokio::sync::mpsc::Sender<()>>,
+}
+
+impl BackgroundTasks {
+    pub fn new(
+        awareness_updater: JoinHandle<()>,
+        awareness_shutdown_tx: tokio::sync::mpsc::Sender<()>,
+    ) -> Self {
+        Self {
+            awareness_updater,
+            awareness_shutdown_tx,
+            redis_subscriber_task: None,
+            redis_subscriber_shutdown_tx: None,
+            heartbeat_task: None,
+            heartbeat_shutdown_tx: None,
+        }
+    }
+
+    pub fn set_redis_subscriber(
+        &mut self,
+        task: JoinHandle<()>,
+        shutdown_tx: tokio::sync::mpsc::Sender<()>,
+    ) {
+        self.redis_subscriber_task = Some(task);
+        self.redis_subscriber_shutdown_tx = Some(shutdown_tx);
+    }
+
+    pub fn set_heartbeat(
+        &mut self,
+        task: JoinHandle<()>,
+        shutdown_tx: tokio::sync::mpsc::Sender<()>,
+    ) {
+        self.heartbeat_task = Some(task);
+        self.heartbeat_shutdown_tx = Some(shutdown_tx);
+    }
+
+    pub fn stop_redis_subscriber(&mut self) {
+        if let Some(tx) = self.redis_subscriber_shutdown_tx.take() {
+            let _ = tx.try_send(());
+            if let Some(task) = self.redis_subscriber_task.take() {
+                task.abort();
+            }
+        } else if let Some(task) = self.redis_subscriber_task.take() {
+            task.abort();
+        }
+    }
+
+    pub fn stop_heartbeat(&mut self) {
+        if let Some(tx) = self.heartbeat_shutdown_tx.take() {
+            let _ = tx.try_send(());
+            if let Some(task) = self.heartbeat_task.take() {
+                task.abort();
+            }
+        } else if let Some(task) = self.heartbeat_task.take() {
+            task.abort();
+        }
+    }
+
+    pub fn stop_awareness_updater(&mut self) {
+        let _ = self.awareness_shutdown_tx.try_send(());
+        self.awareness_updater.abort();
+    }
+
+    pub fn stop_all(&mut self) {
+        self.stop_redis_subscriber();
+        self.stop_heartbeat();
+        self.stop_awareness_updater();
+    }
+}
+
+impl Drop for BackgroundTasks {
+    fn drop(&mut self) {
+        self.stop_all();
+    }
+}
diff --git a/server/websocket/src/broadcast/types.rs b/server/websocket/src/broadcast/types.rs
new file mode 100644
index 0000000000..b4512ced6d
--- /dev/null
+++ b/server/websocket/src/broadcast/types.rs
@@ -0,0 +1,14 @@
+use std::sync::Arc;
+
+use crate::BroadcastGroup;
+
+#[derive(Debug, Clone)]
+pub struct BroadcastConfig {
+    pub storage_enabled: bool,
+    pub doc_name: Option<String>,
+}
+
+#[derive(Debug)]
+pub struct BroadcastGroupContext {
+    pub group: Arc<BroadcastGroup>,
+}
diff --git a/server/websocket/src/conn.rs b/server/websocket/src/conn.rs
index f84cbc90db..ea63b48bb3 100644
--- a/server/websocket/src/conn.rs
+++ b/server/websocket/src/conn.rs
@@ -34,11 +34,7 @@ where
         user_token: Option<String>,
     ) -> Self {
         let sink = Arc::new(Mutex::new(sink));
-        let broadcast_sub = Some(broadcast_group.clone().subscribe(
-            sink,
-            stream,
-            user_token.clone(),
-        ));
+        let broadcast_sub = Some(broadcast_group.subscribe(sink, stream, user_token.clone()));
 
         Connection {
             broadcast_sub,
diff --git a/server/websocket/src/server.rs b/server/websocket/src/server.rs
index 55e44c1d8e..39e0e19d0c 100644
--- a/server/websocket/src/server.rs
+++ b/server/websocket/src/server.rs
@@ -5,6 +5,8 @@ use axum::{
     routing::get,
     Router,
 };
+use tower::ServiceBuilder;
+use tower_http::compression::CompressionLayer;
 
 use google_cloud_storage::{
     client::Client,
@@ -60,7 +62,8 @@ pub async fn start_server(state: Arc<AppState>, port: &str) -> Result<(), anyhow
     let app = Router::new()
         .merge(ws_router)
         .nest("/api", document_routes())
-        .with_state(state);
+        .with_state(state)
+        .layer(ServiceBuilder::new().layer(CompressionLayer::new()));
 
     info!("WebSocket endpoint available at ws://{}/[doc_id]", addr);
     info!(
diff --git a/server/websocket/src/storage/redis/mod.rs b/server/websocket/src/storage/redis/mod.rs
index 7621164526..bde957cf7e 100644
--- a/server/websocket/src/storage/redis/mod.rs
+++ b/server/websocket/src/storage/redis/mod.rs
@@ -45,21 +45,27 @@ impl RedisStore {
         &self,
         stream_key: &str,
         update: &[u8],
+        conn: &mut Connection,
     ) -> Result<(), anyhow::Error> {
-        if let Ok(mut conn) = self.pool.get().await {
-            let mut pipe = redis::pipe();
-
-            let fields = &[("update", update)];
-            pipe.cmd("XADD")
-                .arg(stream_key)
-                .arg("NOMKSTREAM")
-                .arg("*")
-                .arg(fields);
+        let script = redis::Script::new(
+            r#"
+            local stream_key = KEYS[1]
+            local update = ARGV[1]
+            local ttl = ARGV[2]
+            
+            redis.call('XADD', stream_key, 'NOMKSTREAM', '*', 'update', update)
+            redis.call('EXPIRE', stream_key, ttl)
+            return 1
+            "#,
+        );
 
-            pipe.cmd("EXPIRE").arg(stream_key).arg(self.config.ttl);
+        let _: () = script
+            .key(stream_key)
+            .arg(update)
+            .arg(self.config.ttl)
+            .invoke_async(&mut *conn)
+            .await?;
 
-            let _: () = pipe.query_async(&mut *conn).await?;
-        }
         Ok(())
     }
 
@@ -490,7 +496,7 @@ impl RedisStore {
         consumer_name: &str,
         count: usize,
     ) -> Result<Vec<Bytes>, anyhow::Error> {
-        let block_ms = 1500;
+        let block_ms = 1600;
 
         let result: RedisStreamResults = redis::cmd("XREADGROUP")
             .arg("GROUP")
@@ -824,32 +830,29 @@ impl RedisStore {
         let stream_key = format!("yjs:stream:{}", doc_id);
 
         if let Ok(mut conn) = self.pool.get().await {
-            let exists: bool = redis::cmd("EXISTS")
-                .arg(&stream_key)
-                .query_async(&mut *conn)
-                .await?;
-
-            if !exists {
-                return Ok(Vec::new());
-            }
-
-            type RawStreamEntry = (String, Vec<(String, Bytes)>);
-            let result: Vec<RawStreamEntry> = redis::cmd("XRANGE")
-                .arg(&stream_key)
-                .arg("-")
-                .arg("+")
-                .query_async(&mut *conn)
-                .await?;
-
-            let mut updates = Vec::new();
+            let script = redis::Script::new(
+                r#"
+                if redis.call('EXISTS', KEYS[1]) == 0 then
+                    return {}
+                end
+                
+                local result = redis.call('XRANGE', KEYS[1], '-', '+')
+                local updates = {}
+                
+                for i, entry in ipairs(result) do
+                    local fields = entry[2]
+                    for j = 1, #fields, 2 do
+                        if fields[j] == "update" then
+                            table.insert(updates, fields[j+1])
+                        end
+                    end
+                end
+                
+                return updates
+            "#,
+            );
 
-            for (_, fields) in result {
-                for (field_name, field_value) in fields {
-                    if field_name == "update" {
-                        updates.push(field_value);
-                    }
-                }
-            }
+            let updates: Vec<Bytes> = script.key(&stream_key).invoke_async(&mut *conn).await?;
 
             return Ok(updates);
         }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>server/websocket/src/storage/redis/mod.rs (1)</summary><blockquote>

`44-64`: **Consider consistent connection handling across Redis methods.**

While this method now accepts a connection parameter, other similar methods in `RedisStore` still acquire their own connections. For better consistency and resource management, consider updating other frequently used methods to follow the same pattern.


For example, consider applying this pattern to methods like `batch_ack_messages` and other methods that are called in performance-critical paths:

```diff
 pub async fn batch_ack_messages(
     &self,
     doc_id: &str,
     group_name: &str,
     message_ids: &[String],
+    conn: &mut Connection,
 ) -> Result<(), anyhow::Error> {
     if message_ids.is_empty() {
         return Ok(());
     }

     let stream_key = format!("yjs:stream:{}", doc_id);
-    if let Ok(mut conn) = self.pool.get().await {
-        let mut pipe = redis::pipe();
-
-        let mut cmd = redis::cmd("XACK");
-        cmd.arg(&stream_key).arg(group_name);
-
-        for id in message_ids {
-            cmd.arg(id);
-        }
-
-        pipe.add_command(cmd);
-
-        let _: () = pipe.query_async(&mut *conn).await?;
-        return Ok(());
-    }
-    Err(anyhow::anyhow!("Failed to get Redis connection"))
+    let mut pipe = redis::pipe();
+
+    let mut cmd = redis::cmd("XACK");
+    cmd.arg(&stream_key).arg(group_name);
+
+    for id in message_ids {
+        cmd.arg(id);
+    }
+
+    pipe.add_command(cmd);
+
+    let _: () = pipe.query_async(&mut *conn).await?;
+    Ok(())
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 04305c869807f9024fc9204b59cb55a2ff08fbdd and ecc177aa6ba00d017124120b772943146d9dbedb.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `server/websocket/src/broadcast/group.rs` (4 hunks)
* `server/websocket/src/conn.rs` (1 hunks)
* `server/websocket/src/storage/redis/mod.rs` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>server/websocket/src/broadcast/group.rs (1)</summary><blockquote>

<details>
<summary>server/websocket/src/storage/redis/mod.rs (1)</summary>

* `new` (29-34)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: ci-websocket / ci

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>server/websocket/src/conn.rs (1)</summary>

`37-37`: **Code simplification looks good.**

The subscription call has been simplified to a single line, which improves readability without changing functionality.

</details>
<details>
<summary>server/websocket/src/broadcast/group.rs (2)</summary>

`8-8`: **Good addition of the deadpool_redis Connection import.**

This import supports the connection handling improvements in the modified methods.

---

`512-525`: **Improved Redis update handling logic.**

The code has been simplified to extract update bytes directly and immediately publish them when not empty, eliminating unnecessary code paths and improving code clarity.

</details>
<details>
<summary>server/websocket/src/storage/redis/mod.rs (2)</summary>

`44-49`: **Good approach to connection reuse.**

Accepting a mutable connection parameter instead of acquiring one internally improves connection management by allowing connection reuse across multiple operations.

---

`50-62`: **Simplified Redis command execution.**

The code now directly uses the provided connection for Redis operations, eliminating the conditional connection acquisition block and making the code more straightforward.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T14:32:43Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🔭 Outside diff range comments (2)</summary><blockquote>

<details>
<summary>server/websocket/src/broadcast/group.rs (2)</summary><blockquote>

`571-709`: _🛠️ Refactor suggestion_

**Potential race conditions during shutdown.**  
While removing heartbeat, checking active instances, and locking for GCS, another instance might connect. This sequence could lead to partial or stale data writes. Consider a more atomic approach to concurrency if consistent final state is critical.

---

`505-511`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Ensure all callers of `handle_msg` supply a valid Connection.**  
The signature change to accept `&mut Connection` requires each invocation be updated accordingly.  

Please confirm all calls to `handle_msg` are updated:

---

🏁 Script executed:

```shell
#!/bin/bash
rg 'handle_msg' -A 5
```

Length of output: 912

---



</details>

**Action Required: Update `handle_msg` Call Site to Supply a Valid Connection**  
The grep output confirms that the invocation in `server/websocket/src/broadcast/group.rs` still passes only five arguments. Since the function signature now requires a mutable `conn` parameter (sixth argument), please update the call accordingly.

- **Location:** `server/websocket/src/broadcast/group.rs`
- **Current Invocation:**  
  ```rust
  match Self::handle_msg(
      &protocol,
      &awareness,
      msg,
      &redis_store,
      stream_key.clone(),
  )
  ```  
- **Required Change:**  
  Update the call to include a valid mutable `Connection` (e.g., `&mut conn`) as the additional argument so it aligns with the new function signature:
  ```rust
  match Self::handle_msg(
      &protocol,
      &awareness,
      msg,
      &redis_store,
      stream_key.clone(),
      &mut conn, // pass the valid mutable Connection here
  )
  ```

Please ensure all other callers of `handle_msg` are similarly updated.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>server/websocket/src/broadcast/group.rs (4)</summary><blockquote>

`192-199`: **Consider potential concurrency issues.**  
If multiple instances concurrently create a new `BroadcastGroup` for the same `doc_name`, they might concurrently read from the storage. While each instance eventually updates the doc, consider whether you need an external lock for partial or conflicting loads.

---

`304-305`: **Review error handling for heartbeat updates.**  
If `update_instance_heartbeat` fails multiple times in a row, you may wish to handle more complex backoff or fallback behavior before logging and continuing.

---

`353-355`: **Avoid returning a cloned String if immutability suffices.**  
Returning `&str` might be more efficient than cloning the entire string on every call:
```diff
-pub fn get_doc_name(&self) -> String {
-    self.doc_name.clone()
+pub fn get_doc_name(&self) -> &str {
+    &self.doc_name
}
```

---

`520-527`: **Consider batching or throttling Redis updates.**  
Publishing each update individually can be I/O-intensive. Batching updates or adding a small queue could provide better performance, especially under a high update load.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9efa145877d2975ef90902a653536551e6d41a3a and 260f753b6f61e740f0561766b682d61cdeebc198.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `server/websocket/src/broadcast/group.rs` (13 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>server/websocket/src/broadcast/group.rs (4)</summary><blockquote>

<details>
<summary>server/websocket/src/broadcast/pool.rs (2)</summary>

* `new` (33-40)
* `new` (228-262)

</details>
<details>
<summary>server/websocket/src/storage/redis/mod.rs (1)</summary>

* `new` (29-34)

</details>
<details>
<summary>server/websocket/src/storage/gcs/mod.rs (1)</summary>

* `new` (71-75)

</details>
<details>
<summary>server/websocket/src/conn.rs (1)</summary>

* `new` (30-46)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: ci-websocket / ci

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>server/websocket/src/broadcast/group.rs (9)</summary>

`8-8`: **No concerns with introducing the Connection import.**  
This import properly references the Deadpool Redis Connection type, which helps manage pooled connections in an asynchronous context.

---

`187-187`: **Check effect of empty doc_name fallback.**  
Using `unwrap_or_default()` can result in an empty doc_name, which might not be ideal for certain Redis or GCS operations. Verify whether an empty string is a valid fallback.

---

`189-189`: **Straightforward conditional flow.**  
Returning early when storage is disabled looks correct and aligns with the constructor changes.

---

`378-378`: **Good trace logging.**  
Logging the doc_name here helps diagnose connection events.

---

`453-453`: **Appropriate naming of the stream key.**  
Combining the `"yjs:stream:"` prefix with the doc_name is clear and unlikely to cause collisions.

---

`455-455`: **Potential panic due to unwrap on Redis connection.**  
This repeats a prior comment. To avoid panics, handle the error case gracefully:

---

`474-479`: **Improved clarity by passing a mutable connection reference.**  
Passing `&mut conn` explicitly removes ambiguity about connection state. This is beneficial for concurrency and reduces hidden resource usage.

---

`514-518`: **Verify correctness of ignoring other sync variants.**  
This match only handles `Update` and `SyncStep2` variants for building `update_bytes`. If additional protocol message variants require special handling, they might be incorrectly skipped here.

---

`97-98`: <details>
<summary>❓ Verification inconclusive</summary>

**Confirm no breakage from new mandatory parameters.**  
The constructor now mandatorily takes `storage` and `doc_name`, which might break existing code if these arguments were previously optional.  

Please confirm that all invocations of `BroadcastGroup::new` now pass valid storage and doc_name by running:

---

🏁 Script executed:

```shell
#!/bin/bash
rg 'BroadcastGroup::new'
```

Length of output: 24

---



</details>

**Action Required: Manual Verification of Constructor Invocations**

The initial search with `rg 'BroadcastGroup::new'` did not yield any matching lines. This makes it unclear whether existing calls are absent or if they are formatted in an alternate way (e.g., with an opening parenthesis).

To further verify, please run the following script to search for invocations more precisely:

```shell
#!/bin/bash
rg 'BroadcastGroup::new\('
```

If there are still no results, double-check whether the constructor is indeed being used anywhere in the codebase, or if its usage is routed through another abstraction. Ultimately, ensure that all invocations (if any) provide valid `storage` and `doc_name` arguments.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T15:40:47Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>server/websocket/src/storage/redis/mod.rs (1)</summary><blockquote>

`44-68`: **Improved Redis connection handling with Lua script**

The method now accepts a Redis connection as a parameter rather than acquiring one internally, which allows for better connection reuse. Additionally, the implementation uses a Lua script for atomic execution of commands, improving reliability and performance.



Consider adding documentation for this breaking API change since the method now requires a connection parameter. This might affect all callers of this method.

```diff
 pub async fn publish_update(
     &self,
     stream_key: &str,
     update: &[u8],
     conn: &mut Connection,
-) -> Result<(), anyhow::Error> {
+) -> Result<(), anyhow::Error> {
+    // Requires a pre-acquired Redis connection to improve connection reuse
+    // and reduce connection acquisition overhead
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 260f753b6f61e740f0561766b682d61cdeebc198 and dbac062b2575dd29e00cc299e8de0bb1b7a241bd.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `server/websocket/src/storage/redis/mod.rs` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: ci-websocket / ci

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T15:51:02Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>server/websocket/src/storage/redis/mod.rs (1)</summary><blockquote>

`44-68`: **Improved Redis operation reliability using Lua script**

The refactoring of the `publish_update` method improves reliability and performance by:
1. Accepting a reused connection from the caller rather than creating a new one
2. Using a Lua script to ensure the XADD and EXPIRE operations are executed atomically



Consider adding a brief comment explaining why the Lua script approach was chosen over the previous implementation, as it would help future maintainers understand the design decision.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bb8a9e4856c3237dd59d4054ec36a83128ee85ac and dd611b5c256e1eb446a689c42a63144653434022.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `server/websocket/src/storage/redis/mod.rs` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: ci-websocket / ci

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>server/websocket/src/storage/redis/mod.rs (1)</summary>

`499-499`: <details>
<summary>✅ Verification successful</summary>

**Increased blocked read timeout**

This change increases the blocking timeout from 1500ms to 1600ms, which can reduce CPU usage by reducing polling frequency, at the cost of a small increase in message processing latency.



---

🏁 Script executed:

```shell
#!/bin/bash
# Check for other timeouts in the codebase to ensure consistency
rg -A 1 -B 1 "block_ms\s*=\s*\d+" --glob "*.rs" | grep -v "1600"
```

Length of output: 194

---



</details>

**Blocking Timeout Change Verified**

The consistency check confirms that the only occurrence of `block_ms` was updated to `1600`, with no other conflicting assignments in the codebase. The increased block timeout is consistent and appropriate.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T16:09:15Z
user: login: pyshx
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T06:05:03Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>server/websocket/src/broadcast/tasks.rs (3)</summary><blockquote>

`1-2`: **Consider adding doc comments for clarity.**
It would be helpful to explain the purpose of this module and how it manages background tasks, enabling future maintainers to quickly understand its responsibilities.

---

`27-34`: **Optional: add logging for better observability.**
When setting the Redis subscriber task, consider logging that the task has been registered, aiding debug and operational awareness.

---

`45-54`: **Refactor repeated shutdown logic.**
Both `stop_redis_subscriber` and `stop_heartbeat` contain similar patterns for shutting down tasks. Extracting that pattern into a helper function could reduce duplication and improve maintainability.

```diff
-    pub fn stop_redis_subscriber(&mut self) {
-        if let Some(tx) = self.redis_subscriber_shutdown_tx.take() {
-            let _ = tx.try_send(());
-            if let Some(task) = self.redis_subscriber_task.take() {
-                task.abort();
-            }
-        } else if let Some(task) = self.redis_subscriber_task.take() {
-            task.abort();
-        }
-    }
+    fn stop_task(task_option: &mut Option<JoinHandle<()>>, tx_option: &mut Option<tokio::sync::mpsc::Sender<()>>) {
+        if let Some(tx) = tx_option.take() {
+            let _ = tx.try_send(());
+            if let Some(task) = task_option.take() {
+                task.abort();
+            }
+        } else if let Some(task) = task_option.take() {
+            task.abort();
+        }
+    }
    
+    pub fn stop_redis_subscriber(&mut self) {
+        Self::stop_task(&mut self.redis_subscriber_task, &mut self.redis_subscriber_shutdown_tx);
+    }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between cbda703196f35e2d398a1b4f8a6b9ef325d9a5d6 and 3ac2d798af08aa2631b6f6ebe25b375250a5736a.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `server/Cargo.lock` is excluded by `!**/*.lock`, `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `server/websocket/Cargo.toml` (1 hunks)
* `server/websocket/src/broadcast/group.rs` (15 hunks)
* `server/websocket/src/broadcast/mod.rs` (1 hunks)
* `server/websocket/src/broadcast/pool.rs` (7 hunks)
* `server/websocket/src/broadcast/tasks.rs` (1 hunks)
* `server/websocket/src/broadcast/types.rs` (1 hunks)
* `server/websocket/src/server.rs` (2 hunks)
* `server/websocket/src/storage/redis/mod.rs` (3 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* server/websocket/src/broadcast/mod.rs

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* server/websocket/src/broadcast/pool.rs
* server/websocket/src/broadcast/group.rs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>server/websocket/src/broadcast/tasks.rs (1)</summary><blockquote>

<details>
<summary>server/websocket/src/broadcast/group.rs (2)</summary>

* `new` (88-195)
* `drop` (741-758)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Redirect rules - reearth-flow
* GitHub Check: Header rules - reearth-flow
* GitHub Check: Pages changed - reearth-flow
* GitHub Check: ci-websocket / ci

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>server/websocket/Cargo.toml (1)</summary>

`59-60`: **Great addition of compression capabilities.**

Adding `tower` and `tower-http` dependencies with Brotli compression support will help reduce bandwidth usage and improve response times, especially for clients with limited connectivity.

</details>
<details>
<summary>server/websocket/src/server.rs (2)</summary>

`8-9`: **Good imports to support HTTP compression.**

These imports properly bring in the required components for HTTP compression implementation.

---

`65-66`: **Well-implemented HTTP compression layer.**

The compression layer is correctly added to the middleware stack after setting the state. This implementation will compress HTTP responses, improving bandwidth efficiency and load times.

</details>
<details>
<summary>server/websocket/src/broadcast/types.rs (1)</summary>

`1-14`: **Good structure for the broadcast configuration system.**

The new types are well-designed:
- `BroadcastConfig` with appropriate derivation of Debug and Clone
- `BroadcastGroupContext` using Arc for thread-safe shared ownership of BroadcastGroup
- Clear field naming and organization

These structures will help streamline Redis connection handling in the broadcast system.

</details>
<details>
<summary>server/websocket/src/storage/redis/mod.rs (3)</summary>

`44-70`: **Improved efficiency in Redis publish operation.**

Good improvements to the `publish_update` method:
1. Accepting a connection parameter instead of acquiring one internally reduces connection overhead
2. Using a Lua script for atomic operations eliminates multiple round-trips to Redis
3. The script properly combines `XADD` and `EXPIRE` operations

This change aligns well with the PR objective of improving Redis connection handling.

---

`499-499`: **Minor timing adjustment for Redis blocking reads.**

The increase in blocking time from 1500ms to 1600ms is a small optimization, likely based on observed behavior or performance testing.

---

`829-857`: **Significant improvement to stream data retrieval.**

The refactoring of `read_all_stream_data` provides several benefits:
1. Using a Lua script to check stream existence and retrieve data in a single operation reduces round-trips
2. Early return when the stream doesn't exist improves efficiency
3. Processing and filtering the results within Lua reduces data transfer between Redis and the application

This is a well-implemented change that will improve performance when reading stream data.

</details>
<details>
<summary>server/websocket/src/broadcast/tasks.rs (1)</summary>

`3-10`: **Struct design is clear and organized.**
Storing each task and corresponding shutdown channel in one struct provides a convenient and cohesive way to manage these resources.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T06:25:58Z

Review comments:
diff_hunk: @@ -448,8 +449,8 @@ impl BroadcastGroup {
             let redis_store = self.redis_store.clone();
             let doc_name = self.doc_name.clone();
             let stream_key = format!("yjs:stream:{}", doc_name.clone().unwrap_or_default());
-
             tokio::spawn(async move {
+                let mut conn = redis_store.get_pool().get().await.unwrap();
path: server/websocket/src/broadcast/group.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove unwrap() to prevent potential panics.**

Using `unwrap()` on the Redis connection could cause the application to panic if the connection cannot be established, which would terminate the WebSocket connection without proper error handling.


```diff
-let mut conn = redis_store.get_pool().get().await.unwrap();
+let mut conn = match redis_store.get_pool().get().await {
+    Ok(conn) => conn,
+    Err(e) => {
+        tracing::error!("Failed to get Redis connection: {}", e);
+        return Ok(());
+    }
+};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                let mut conn = match redis_store.get_pool().get().await {
                    Ok(conn) => conn,
                    Err(e) => {
                        tracing::error!("Failed to get Redis connection: {}", e);
                        return Ok(());
                    }
                };
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 9b17ef4
created_at: 2025-03-25T14:32:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,9 +38,9 @@ pub struct BroadcastGroup {
     awareness_updater: JoinHandle<()>,
     doc_sub: Option<yrs::Subscription>,
     awareness_sub: Option<yrs::Subscription>,
-    storage: Option<Arc<GcsStore>>,
+    storage: Arc<GcsStore>,
     redis_store: Arc<RedisStore>,
-    doc_name: Option<String>,
+    doc_name: String,
path: server/websocket/src/broadcast/group.rs
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Ensure validity of mandatory fields.**  
By removing the `Option` from `storage` and `doc_name`, the code now always requires these fields. If `doc_name` is empty, it may cause unexpected behaviors or collisions in Redis/GCS.  

Would you like to verify there's no assumption about `doc_name` being optional or non-empty using a quick search?

---

🏁 Script executed:

```shell
#!/bin/bash
# Verify that no code path expects doc_name to be optional or handles it as if it could be empty
rg 'doc_name'
```

Length of output: 5741

---



</details>

**Attention required: Validate non-empty `doc_name` before use**  
The recent changes have made `doc_name` a mandatory, non-optional field. However, grep indicates that the configuration fallback (`config.doc_name.unwrap_or_default()`) may still yield an empty string. Since this value is used in key generation and storage access (e.g., in Redis and GCS operations), an empty `doc_name` could lead to collisions or unexpected behaviors.  
  
- Ensure that when loading or initializing `doc_name` (for example, at the point of configuration in `server/websocket/src/broadcast/group.rs`), its value is validated to be non-empty.  
- Consider replacing the fallback with an explicit error or default that meets your application’s safety guarantees.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-25T15:40:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+use tokio::task::JoinHandle;
+
+pub struct BackgroundTasks {
+    awareness_updater: JoinHandle<()>,
+    awareness_shutdown_tx: tokio::sync::mpsc::Sender<()>,
+    redis_subscriber_task: Option<JoinHandle<()>>,
+    redis_subscriber_shutdown_tx: Option<tokio::sync::mpsc::Sender<()>>,
+    heartbeat_task: Option<JoinHandle<()>>,
+    heartbeat_shutdown_tx: Option<tokio::sync::mpsc::Sender<()>>,
+}
+
+impl BackgroundTasks {
+    pub fn new(
+        awareness_updater: JoinHandle<()>,
+        awareness_shutdown_tx: tokio::sync::mpsc::Sender<()>,
+    ) -> Self {
+        Self {
+            awareness_updater,
+            awareness_shutdown_tx,
+            redis_subscriber_task: None,
+            redis_subscriber_shutdown_tx: None,
+            heartbeat_task: None,
+            heartbeat_shutdown_tx: None,
+        }
+    }
+
+    pub fn set_redis_subscriber(
+        &mut self,
+        task: JoinHandle<()>,
+        shutdown_tx: tokio::sync::mpsc::Sender<()>,
+    ) {
+        self.redis_subscriber_task = Some(task);
+        self.redis_subscriber_shutdown_tx = Some(shutdown_tx);
+    }
+
+    pub fn set_heartbeat(
+        &mut self,
+        task: JoinHandle<()>,
+        shutdown_tx: tokio::sync::mpsc::Sender<()>,
+    ) {
+        self.heartbeat_task = Some(task);
+        self.heartbeat_shutdown_tx = Some(shutdown_tx);
+    }
+
+    pub fn stop_redis_subscriber(&mut self) {
+        if let Some(tx) = self.redis_subscriber_shutdown_tx.take() {
+            let _ = tx.try_send(());
+            if let Some(task) = self.redis_subscriber_task.take() {
+                task.abort();
+            }
+        } else if let Some(task) = self.redis_subscriber_task.take() {
+            task.abort();
+        }
+    }
+
+    pub fn stop_heartbeat(&mut self) {
+        if let Some(tx) = self.heartbeat_shutdown_tx.take() {
+            let _ = tx.try_send(());
+            if let Some(task) = self.heartbeat_task.take() {
+                task.abort();
+            }
+        } else if let Some(task) = self.heartbeat_task.take() {
+            task.abort();
+        }
+    }
+
+    pub fn stop_awareness_updater(&mut self) {
+        let _ = self.awareness_shutdown_tx.try_send(());
+        self.awareness_updater.abort();
+    }
+
+    pub fn stop_all(&mut self) {
+        self.stop_redis_subscriber();
+        self.stop_heartbeat();
+        self.stop_awareness_updater();
+    }
+}
+
+impl Drop for BackgroundTasks {
+    fn drop(&mut self) {
+        self.stop_all();
+    }
+}
path: server/websocket/src/broadcast/tasks.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Ensure graceful shutdown before aborting tasks.**
`abort()` terminates a task immediately without granting it an opportunity to wrap up gracefully. Consider introducing a short wait to allow each task to handle the shutdown signal before aborting.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-27T06:25:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-25T14:27:59Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes modify how Redis connections are handled across multiple modules. In the `BroadcastGroup`, the `listen` and `handle_msg` methods now acquire and use a mutable Redis connection directly, eliminating the need for conditionally checking a document name and spawning separate asynchronous tasks. Similarly, the `RedisStore::publish_update` method has been updated to accept a mutable connection parameter rather than acquiring one internally. A minor refactoring in the `Connection` struct simplifies the subscription call in its constructor. Additionally, new modules for tasks and types have been introduced, expanding the project's functionality.

## Changes

| File(s)                                                                 | Summary of Changes                                                                                                                                                                                                                   |
|-------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `server/websocket/src/broadcast/group.rs`<br>`server/websocket/src/storage/redis/mod.rs` | Updated method signatures to accept a mutable Redis connection. In `BroadcastGroup::handle_msg`, the Redis connection is obtained once and passed directly for publishing updates, while `RedisStore::publish_update` now uses the provided connection. |
| `server/websocket/src/conn.rs`                                          | Condensed the subscription call in the `new` method into a single line for improved clarity without changing functionality.                                                                                                         |
| `server/websocket/src/broadcast/pool.rs`                               | Simplified retrieval of `doc_name` and `doc_id` in `BroadcastGroupManager`, removing unnecessary conditional checks and default value assignments.                                                                                   |
| `server/websocket/Cargo.toml`                                          | Added new dependencies: `tower` and `tower-http` for enhanced HTTP handling capabilities.                                                                                                                                           |
| `server/websocket/src/broadcast/mod.rs`                                 | Introduced new public modules: `tasks` and `types` to expand the module's interface.                                                                                                                                               |
| `server/websocket/src/broadcast/tasks.rs`                               | Added a new `BackgroundTasks` struct to manage multiple asynchronous tasks related to the websocket server, including methods for task management and proper resource cleanup.                                                          |
| `server/websocket/src/broadcast/types.rs`                                | Defined two new public structures: `BroadcastConfig` and `BroadcastGroupContext`, facilitating configuration and shared ownership of `BroadcastGroup`.                                                                                |
| `server/websocket/src/server.rs`                                        | Introduced a compression layer in the `start_server` function to enhance HTTP response handling.                                                                                                                                   |

## Possibly related PRs

- **reearth/reearth-flow#978**: The changes in the main PR modify the `BroadcastGroup` struct and its methods, while the retrieved PR also alters methods within the same `BroadcastGroup` struct, specifically focusing on Redis update handling.
- **reearth/reearth-flow#1116**: The changes in the main PR modify the `publish_update` method in `RedisStore`, involving a mutable reference to a `Connection`, aligning closely with these changes.
- **reearth/reearth-flow#987**: The changes in the main PR are related to modifications in the `BroadcastGroup` struct and its methods, specifically in how Redis connections are handled.

## Poem

> Oh, how the code hops along with glee,  
> A brighter path for Redis we now see,  
> Streams and messages flow without delay,  
> With clean new calls leading the way,  
> I'm a rabbit proud as I twitch my ear—hip hip, hooray!  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpADK2VmgqyjcEj2wAbb5AokAI7Y0riQ4rjedJAARAyw+AEAFADuJAKI+AwA1iS4AJRc8My8+FKQNnSoTBjkYvD4WLCYtN7wGEQoWAIU+Gi0DGiIYdjctNTSkADaAGIAMgDyAOpgAEIAomAAjAAMAOwAujGQKUOQiNgCzOo09HLhsJ7YiJSQ2UPYRGjXVDotKMh4EoMOIAGbwaL3XCPSAYJwCV5bJEATg0MBhtnQxWQBEgZGaGAYnmhngJrXanXwoIqVWQNTq4kayEQ3FE8HBg188naD08NRoAA8wlTID0+gMhiMxhNEBp3DClIgGBR4NxGVhShIgdEeSTrHZaPhJhh8GF2gxvNglJAlLhtFF6O1QYlmNQGlgAl8KLQKbzzqyGOz4AxGATSMhXUoADQnZphAKu9o4/CizyG8gxxKQBIpP3xTDhk6UYmhOhogCCtB96rQXJjJIC6EbJvOypIZEQCVwyCz3Bc4gYPhcPHw7W72fgRFgbSnt0gzr4AS1JBSaI8kDYzBTL3qjQB6G8mTxJVwshj7R9g3EHQe1FhKf61fdtfvNB7fCYrHYyDSjc12toOUDBmLNQWwCgGxtPJ7UQesMTsQZuk8NAGCJRAXnoO89WdXx8BSX0AFUbDmLgplgXBcG4RAOAAehooh1FgS4NE/GiAhIftYDY9tOLAUFvDwmjuB8bwaKRLZkQOJJyMo6i6IY6FmNY9jOO4jiINgPiBJSISRLElF8iA/RjHAKAyHoEU0DwQhSHIKg50/NgQS4Xh+GEURxCkGR5CYJQqFUdQtB0YyTCgOBUFQTAcAIYgyGUByWCczh/DQXMLmcVxRR8xRlACzRtF0MBDBM0wDBeCgpAoGi0gyLJclwGjEAoBgaLFfpBmGejelGDQKGogwYkGgwLEgcsAElYrsiZ6HS11MpFfMOmkeU+TDSYx16WhsCJdBYRXKDWQwYEGHkPcAAMlH6bh8HwbwAH0Ah9aiOAAYUaBl3TO9Ajo3RR2XkPUztWXp2slABxbruC+4YKG2sJcQCYJ4GbRowHwNVnz8U9WWQBc/TO4ZEjQUgvv+C6sjujBvhIL7wRIbxaFldE+SZXBYbELM2GhRRkDO8gUlJn6zvw6E7sJqgSezNBygRMhIFGcY51xFCiTVXkXnnCEGeQH0AjEbx5DOE0MDRjHGhfPsqC5ygmfXM7SFwO7DQYSnqa+rmEnoZoZHbLAtx9On6ARvJwKwNBIDO+w2YpL6kxofp+GpKKzoWM2MGAKOVQ6PQzpjAJ+I8309UW0gunx53XbYWmtcAlbIAEhiQzxsk2hvSonoUWpC6ZKWfblmGOOYVuy2ZiO2mGMh3byT371zFCkYCZBw+YPAVCiGkO/pbuw/hmFhn7b76D7dD1vhlNAZbkg7uYRAiCn7naBjFIEHiXvUzlhXpvCFMEzKTx0ZrH4eIohsi4yzOTF2VMq5rhhLwEgWp8DPBQCUKISU3R7mhHedoEhbpSBmn2FIGBfRDFkISWAvQTRILtIgUB85eyXHHggG8n83xv1lh6Eg3BvAoWiCLWANpkYeUYLWQ838vACEYfLaUrDnhF1gb0LUSh6Dt2qO9beMZEDFC4f9ORnhKC9D4C3X0pQ0K2xhATJiuBDSEPvjPb2B4jzsKkYraIwcC5iHVqtAs0gYwdnAnIrC5iK5QJpvuZ40RdYeQNicRiiDhRpxfMAnIsojALEqiIuCnh/ZBivO6ZA+JMA7WLtwlUp5D54lBByCEhJTrUj1CowEIJlC7iwHw3U5jgbig6rgCGiCobIK4SQNB6osr+CGWUX02Au6mJcKdBJbQyn/E0SgnRN5i6NDZrdec2kgIjXLN4Gg9k8liL1EoC0Lh0EYB7NSEgAproQWiPQiRwY8QgnUBCRARgoAAFlp6KHOJOKmuBwJPGkXQLgZ0SGEnnE0FoURr632AFYLgVhegECYN4PQSQNA4vyLHLAQMQYSmGL00Y+KI7lUqtVdImQch5Eas1VqRLuldT6b1RAX1ABJhBHKFTdYVHXhTfIgSKUVoqyLdLFOKNAxnpFwAAZCvMIb1pnqjxWXQlXTwaQ3JQTSgVKaq0vqgylqbViUNSIJDdlZ0DA/L+TNQF1AQXOOmhC4SAgYWQAdk7CmISkhypeN4UE+RdB6EgKndUGdo7Zx1Z00GJLtXqspZQaltU6UNSaia5lkpWU9T6lyiObqPVeuCdTP1Aag0hocFGu+6rY1mtJf0nkuqKrJoNXVelGamWas6hatleabWQF+Q/AFRAgVOpYeCnliBSF8r2ikbFuKY1ZvjX0nVSaqptrTcartcbzWWrzZAblkLp3QtBFgfmC7pXnAIBLEgXByzNWAGDBgiAo6JBIHoGMJa2BcEzhSNVTa63dIbWuvVraaXtvTYy01LLe25o5QOodM9NGjsdY2CdtAIW8o9XwsWN7iYkEvQBglQGtWrsTWBjdEGt2dpg9muD3ArWHqnTOnDjE8NE1IJejRN672jUfc+19vHP2QClcRiOpGV1kooy2qjqajW0eXXuvtCHBoxC+aVddKbDUdsZfSdlHABpDT2RNWy8VoizVmYnUM3jPkGHXCXdaWB1nWmwbg5CAKVkckudZvUFwMjKlVCMtsPmTHSB/Ixdpng+Yrlsf8kUgNlUfUaNDNmcMYH/xVAxKmfgtGoPYD58JmFzicIuTQeu7RiQpk5Fjcx/mlQqgRHFiyBK6PDDugx3OsZgz8PNJaJRfpLbU0ObzTRGBshdYJmzQeXWybhIoHdAguQMAsQEuQJIeKYGoFODrRo/9Kobh8OIQZFXyCwW66/PrVpXEwnCb58xFo9sbea/wAl83Fv4GW2dNEddsmBwGflkEPmajAheDiBAjTlYAo6OvYeX7BH6x8iI30Z02uOwYxoergWERJDG9kHjARvgxne0tsgq2nv5DVXwuJfoxyUDYD6CYp3PA9A4qArbdI1o2iNMgE0YRayHL9GBQkgD1CnT4HqIb1s+qpmMUMDCJy6uXAa/AJrG47UAG4UBhDYAEaJA9vjD3Byza0eNzmlMNj9GogYXhnZSfZx4jYXCeBbI55MfoG7Bl8RQAxUsBUUkzB+DZvQ/D8TwugJeYyLgHPu6gP7wZLlAXMJYfZhzLnu9OaIEp6frO3PuXOJ5bQQzsHectAdAA5B8Bz4rHNxJn83OeRR58SAXvgbqi+vIiB8osjZIyeHaagAOoI5RqY02AIwWnN0KcZeLAj3Eno0X9gZoz6mTOTXMzNJwc1ak2aWnZu27ekCwDuhOl7UWI4NLfQEVL7Mwj2KcfH1xD5UKcP54du0EjPD52LISKru0zpJbbxfSCAiAeJnBRSPjvLmx+BS55CUB5ycLcKBhrLyLwINBIJ04UA5adzJZhwMBIyaIjKe4MAZYKDOA/S3KiB4DuhvxsyYCEHujRCgi9DMDyxjadDhwNI8Cqj0yVZiLhxzDYDhwq5qyYJhCsgUALg3yiimj8KAwAAa5YAAIkoYLPQGdOsPIVYGNDYOsF9I5C0I0rtOwevG5rko0BzrvqXAbkPJVsbmQa6BQQKFQSMmFuhKMjUJkG0IrL6OoF0FDiYchCevEBQogsyIFmrDVkBGNM5uYoTrQHdC0IkTkM1pkqKAJDkEXMUJ4HjPEb6GwOhARsgPfr7M6nOMwSwJAFsAAKw7A7AbjwC+BICiCNCMxiJbAABsdRDRTRO4rRTMdcZ08RiRvgeGhOzATs1AaAL29iUyfkRAe2xhgKOSmA8MVAVyUhlyT8juxIqBCCSCLwwQJeiSiU/wlBg4IyJRcsAQXCPC9AfCu0ghwhERu8d4SStCeMeotySANAv+92ngNh5SAQ0c8CkwE6RhwhFI686MNeFh6IqAIhYQ4IfUYQlUQY60dSe8023weIAoPxS8P0JIHCIJXkfo7AKoPims1eWcnQnxCQGsDx2ENcsI1MsQE6xwuIn43AjRuxX+0gR2kOD4FWwwuyKe1JOedeCoWeZWxyTedyLejybeDCLyJe4gZetqw6KGY66GYKmGBalwEerGZ64ijCJ+YKZa9MoIBOg8d0uQsg8qMMxOYK8qUw2AAAHAcOJjDHDBfrSFfqEk2pPtRtPi1LPqQPPkgIvooExkeoWthsaYfp2GaS4haYGtad8LaSQPaZAP6mzE6S4i6e6QcDKu9PKoqpAIAS0l6Wlh4mdJfrxqBrJtppBtumGSQBGYgFGYBP2lAAAGouDwBrygouJ6lnRRBhASJ1QIqQAAC8VRtROw5KHs/yQxHECRSRKEE2Zc3ptZ9Z76jZ+qwZumoZ+G4Zj0kZS+B6R6456RU50hc5nRdRS5dqEcwxG5KR25NZYQdZfpDZMmh58mx5jUp57Z55nZl5qmQ0NqmmlGzZNG0GSmQkN03gy+o+a+Zm9kFmW+VmC0a0++MIbuZcgMkmPSkM3ymABGFAscKCQyBWrhKoc0jR3IGAOC3g5QeoRiN4CWQSPqbskAEgA5Q55Seov8AlfgIoUydQ4WVmIOUB2B7xZingcC+xgINFwyNB1BbQAAXtEGdOyPXHkA4CwIRt+iQMGnOQxndI9uQBoMWrxWwM9mIogGgKCPTPILcnQbWaZV1s/D1goD4PQFEAnLiNdL8eIC+GdJXuQF9GJSEGYqgFcVgMstov9lDpEmAehICklNZhApXKEhUawfbJDFZWtiQLZXkN6pAqWninBEgrwdcECrop3E+NAaGCAkBJWC1TlgbGkcueoSFjQB1gmolWUU/mMluOxbdhrCKGdFMikFQNwHdIkE7CQKCFZAco5c/NcXkBSVqDeLlUCN9pADERPP0OeGECaLmOlZEIbJlaOvYYvEdjlRjnZVVQ5WqriAdbQD5S/Pwq6IbIeCmEFUHCmKFccX4JFXtjFbWHFbLswgrjyPVkECECCPwBITnvpXzn7q0NEPTC8FtQEEBA5nhQ4imFdtaCJeMmJf8RJkpg2sqoKLgDfj6UoOCEQuqNsX5TtjwEuOgYgNEqzZVncHgPuC2GtqQHwFJYUa4KQVoi3tehMNlXjBqrusquCDWiNf0EIM8ErD/KtVEB4tCKgKJbWJzevOslcnaKjWdEoesDMOWARHMNAHdEoQsC9HdGNKoeNdoFcvLGQt4rXAYGksoL4JkhrPHuYb7TYcPHmDlKMo9NtJMtMtJZlApeUslW0KCLIE1fyCHtsnhDGALgkJ8HIY8MxZsltEUjCCqDQtZlMpfOoVFaErFU5owJQHaDyPyB5fbnsuKeqBnlKQ3v3bnvKQ8hZEqc8sXm8mqXZhqchg6sCjqSOa6gacaReuLHxg+gwE+i+v6SJmBRxgEPeo+nuQEHoNWbfrTd2qRX0uRVTBLQeeBoBVBpmtfUhbdDGfqe6mvSuLjrxsfdvYJnvQgU9IfZvSfb+e+ufeSjud+SRQ2nfZRY/XJjpi/Tumau/ShT2YOi+VqWhsOS6ixqelgANVfAxqmVadzi7ECA6WzMGkVDSNHrgMAFvcAPA5DHoDnJ+ZfSrfWmRRRQ/f+U/ag9umjpg5/cekaSQ4ToNeQ/6paV+hTDQzmTDPQ6GpUEwyw4+uw30pwzA1+VfbuggwI5QMg3BSGegyytdB/dg0hv8ng4vQQ7cBCsaXrLIBaIRvI2mW5EIGWSLfYJaRwBwNALIKyDGNfDtcGNRDmUOiqC+uJnQX4WdIg4I8rTo6MCk6Y0Iygy2Ypm/dY1gxysxmdK46IO41EBQ5mMIH42EAE4GkEyE2E5ABE9HC+vKrE1Ewk78N+Zk1RXQnwLw8Bvw/fVk4GbBVPkBWIwU1aivmPhPuM0eQ1C9C4AsRoAQEPIZmhSnqZnFJhZvhlDvm7nXIRefgASs/gGsywN4NXKYSxe5n6JASMglikCmPzAdOZGQIGGCUdK8IDFMEoIdMdB8gcNDNvKQcicMOgFWHJfuIDAQL+PoZhRogGOifcYxPxTbDQTEDsBoNURoAAEwxCkF9E/SPM0GoBwt4SUBgAyT9LKgTCc2XY6yrVC0xLQgYt9RYs4sdEEvHD/Dk2TDYQcSOOxBcmLyaKow9BEvMxO4oxQTjmyl8AR0SkpjfHDC+gAsfOEjd5ZiyGvA7jD3cU5E8lARTDwuvBznYu4u8sHAGBmtUsUA0sUTcCzmQAADeHLErWAlr3LvLMYrl+DyAc5UwcQLAcCmVkrFAMQBwkAAAvraw7tIMhNC8PXnv8NhFMi0rWGLjTaUKAROcxUwBQPcm6CgZ4NAA6xVj0FZvy+OAABLQDQBWC4m/Fevna+Wvz/XzgoSNHqBM5aIKLRCcWUjUgNtNtjJHHDAEn0CLzXRXLSBJ7DRilp7D2SlpjSlHI9xyn56KkmkqnT0fJfKQDl77Sas/M1JQtKIQrmt8A+vWuEtmMTNLPnOXNDzWpQAntXWcJasnSXuTpnQ3tOuUSuseuVReuutWs8uEv+vCsgpBvTChslDivuhgBSsxuxuPuLM0TLMUCrPrPXOzPQXj5lQLPP2iOIUQWbPGbbPr57PnDYXzTUhHOJsCKVL+GbTbSCsvNzp7shj+w+CUkAdDCgJqERzYzSBdZrsUqkciN5O7pdlMbghRAZYaxks9xpuEkwj8dRAADkkOlAa1O09wIieExCL+7hUnwuWbCy3egt5AaLRtsRLuK40SAr9A2nC7Rgyeo0fdte58g92ehrNyo9revHnepes9x7+0Hnf7o5ha/s4QwniAGumHZHcnGDEF77UXuYMXj4/78X/y4nyXqXsnCFb9mXhHEAxHQZaXZX8n1C7OfUVHq+NHGFX8lmjHVhZe5YPHNgOtv0W068I1G0ignHj8UEbNxCEmm5cGR00ASXzNhtcYG4JjEYR2qo68vK5CjQYRiXNCyAeuX8UOT7JWsmxLBj/IPtuMNcYChicKxCYcpwAQduo1EuwnhdG8CJyu2Ovz735S4cjw/YCIWEf3HEr88uWQg5c4jJjwyMUeAYnkAJli1iJDBI5Ah4pBQMM33Uc3C316l9/4iou0nhX54C/MX0uI7Q7y2bOlDzT3cU7hE6fAtbzIyPeEqPmA6PZtyEgNB42kTVLwFEvoIoXBWOjWrw/wgPEEwP8MSXDw3UU4+MgvD0tIYs334vfTZMyvUvuAMvzW52s7hcUgBsRNWno3tzFo12EYdq7uhMLrF4kgQIQhWMSXEKdvKvoDYvqupjMYU26Md0OvevhdQs7vqUzuduyZEwVFjL/Cl8fonYeAKPPNWQ4WoytuPoN4kJqGtWCvpdfJLIRvSPif7Pu+6P076AAgLeTVEPgYR3SXFYKbmMPV/+ofvgZ+yAhPY1hyDVTOIie3tCjhBGdwAMWJl9PIkJMOfIIimPShvQ/SiTZoqlgySUTB4Cqw2PiCuP+3XWfiNJt4/Ovg/fS8jYdvrI9xjwYcWAcchS/8mJAJBjg+c/Z/mSAQ9wpQW414nQpQEhUeiCzUWSJjJKKb32wh0gEweLZGHjni4QUgt3P0LQFITfAXkg/UgNlRFACAN+cxI/r4iuT+IUCgSLMsIiQgK0x6PGdGM/3KQeNMA0QUYOgG4Df8VQEwE3l5yXY+cV2fnOARu0bzBcd249MLqqUPYDpM4PpPLnFwNKwNIA6/HILN1oDzdt+2TcxpM0QoNdZQtjF8iIJXrf1z0v9MPs93CyR9DkXAAAFKjgMAdbOFCQGAAbYRMOghnogDFhs9CEi2AUFwCWwNAgmwRIJiUCVBBMAmPzCgJYPyDn1K0dTUEDGgwFb8ROiGNQVWHy6r0kqFVA+l7wRAUA/U5ZctPWGE5GCTBZggVBYKsEaIHBGAJwS4M+xuCOAHgjgF4IYA+CPm/gqweJix5SCceMghblEOHTqCv6HqbXupF17CtUhItdIf3yyHtAchONAISJgT5WJ2exQ7+NkDKEVCqhNQvweMIaGSDsg0g2QZELnr/IOhZ0QtMaXd6JD1e3vFIQqgGGWlVh4QlodvzaEzxdh+wpKgQEWqB8+hZwsIOWkuFNDN+1wrYTg3aExDRBmg69P7xsER8me/Q94RcLCFfC5imwhDNsMwgAiNBXQp4SMW8AQiSsgaT4esOaFwisuY0NSnRRoK7C8skAWfujH6YSCrheI+QU+3I5v1lBMzBEbFxcZYBaAc/DER8J1TkjG0hIoHCMjSbUjWho+IjvMybJ0j0uLKIrqhWo6jQdmU0OcB10OZ4UjAPXN5kp2izSiD0w3EEBxyJDjcO2/COzlx1BrKkQwsDODhChIpq1JwonQZmRlGAM0PKR1O2NaMaDq1FuIrAVjiG450xtYbvECndDIBDlvqH3SvrdA4hX8joCeT/rGGnj6sQK+4YMZ/nG5kxvKwfNGoAnx4BIdcaAeQAkAZh+gQk1mcOM7CcDsBMe6TbgE6KFCeinUl3JMEsUn6ax6Y43Iqquhj77hk4W9L6EkGoAsAXk3+Z7jtCYBTJbgwaa6JgX4KGM+GHY7toGAWSltOgnYZ3BZEIQ2wEALrI1jOKGbkZr+fxFCL0HcIrwDkG3XYvESZirBZC+POGHBwBy0VUagMJQukE+AU9umlIjiJoleBKABAnwbLEQGD7qE3RGAD0ZezkoiJ5AfkSQIK3MQvRSqb47QLgELrQDfQdJPkDIxoIigto2iK8P3kto397c3nVPLCV9pSczkgXWUtwIVK8DD8U9LvOqWy43iwCSIzoeIOAmgSxm4orDpKOzRaj4RjE8QfcLEEGMqxNYpmrSO4l1cMGfE61CKKq5iiAKpXUMpRhlEtc5RtHdrgx2VG2ZjmJNEbpXQ8xvNuEsgV4HjDHbNsxW4Wclk5wpR2gIIYsSjLTEzZPM7+0nLibV2UlncD0GotEPYBRbeY6w+MAJhVGDAkBVg2ARon5DtFvQkOVkxoHMDzFZMCq+MG9naJvYB9nWiLGUE2H7wlBqJmPNAHQK+jdRBcNQdWuBB8xx4/oqVMmoSH6weZLJEbLAMZNeD3B8wVPMthHA0AtSUhwUrUESHCmRTKAQTC9IZB6lJAYp4bL1glJMkUARpv9SnGqj6rCJD+LlQXF8TxLqt9qGgXDPvBoB/0JgS0u1JYTU5YACkv+ewuuj07w8mQpIe7jeHuDJjW4nQRqeBzQmQBzJN0+dsyHYBiILQ1SbsIu17psCe4ZEzgUF1xI8D+AE9DvPwIYmV4DwIM0if53XZD1KJkM6idDL4EHtJgv4AAa5liKD52Qw+SrkVBKhmQfolkayBpIShfhnIKUNKFpI8I5R/IagfKMFDJmhQHC6gO6ECDsG80VwdAPDAfGMgGAuZogBgFsD2B7A0AaADougLqK0AdgUsrYPiwAAsqsnYAIGln4tkQasgAMxbA1ZHRWgMiFoAIhzZhUQwFzORACApZq1Y2ciDdI1F9gyIZENUWqK0B9ZHRfWW6UGBoAdg7UN0vrLVloA1Zns/Wfi3DghRTIkAb2QwHxYJzqiashgMbLdJ7BkQdRfFvrJ2AMAlZ+LVWWrKVk7A3Z6c5EPrOqIkAC5bpK2WLNjnIhVqaAI2dUXTl7BaAusvYJXNBCZzM5UcjotUQrneyPZWwEgCbI1loB9Z0czmbHPxZdFQQnc/WQIA6KggOiI8vYGrJ2Cggdg1RVeXsA6LyyOibpfFrQFXl5z2wAgSWU7NrlczzZKEHYB0XxYCB8W1RTuVWF1kkA6iqEXWQ3LdJKAtZds7WWgHVlbABA9AGORAEgBdyi5ewfFqCAED3yA5s892VrN1kMAB5ecsObQD2BukH5+LHBQiFBD4tr5sc1QG6TQANy1ZbpHeQwEjn6y25tAaombI3nJzRA3stAMHNVlul2wVClCMQogVVhV5IC6oknI6IkAR5AgNWcbLllulkQqcvuYbMkX9yQ5IcnYPiyIXgKoADAUBWgD2A7BDZyIZeRXKrleynZTciRaCDIXyyG5oISOZ7ORBoBPZHRPhVAEnkJysFpireWQqAU+yQFy80RQiGfkCA6F1RZ+YHJflsLa5dciBY5B5l8yVey4NIAkXMhWygAA=== -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=reearth/reearth-flow&utm_content=1119):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: netlify[bot]
created_at: 2025-03-25T14:28:05Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *reearth-flow* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 3ac2d798af08aa2631b6f6ebe25b375250a5736a |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/reearth-flow/deploys/67e4eebb7549360008dcd7db |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-1119--reearth-flow.netlify.app](https://deploy-preview-1119--reearth-flow.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTExMTktLXJlZWFydGgtZmxvdy5uZXRsaWZ5LmFwcCJ9.CNfEKXQdorv5vjMk79ovpXCgvYTx1egN_ZSDXhurpCY)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [reearth-flow Preview](https://deploy-preview-1119--reearth-flow.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/reearth-flow/configuration/notifications#deploy-webhooks)._
author_association: NONE