codeant-ai-for-open-source[bot] commented on code in PR #42074:
URL: https://github.com/apache/superset/pull/42074#discussion_r3584621140


##########
superset/commands/report/execute.py:
##########
@@ -187,14 +187,15 @@ def update_report_schedule_slack_v2(self) -> None:
                 channel_names = (slack_recipients["target"] or 
"").replace("#", "")
                 # we need to ensure that existing reports can also fetch
                 # ids from private channels
-                channels = get_channels_with_search(
+                channels_data = get_channels_with_search(
                     search_string=channel_names,
                     types=[
                         SlackChannelTypes.PRIVATE,
                         SlackChannelTypes.PUBLIC,
                     ],
                     exact_match=True,
                 )
+                channels = channels_data["result"]

Review Comment:
   **Suggestion:** This migration lookup now calls a paginated API but never 
overrides the default page size, so it only inspects the first 100 matching 
channels. If a legacy Slack recipient contains more than 100 channel targets, 
the upgrade will incorrectly report channels as missing and fail even though 
they exist. Pass an explicit limit based on the number of targets (and/or 
follow pagination until all requested exact matches are resolved). [api 
mismatch]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   ❌ Auto-upgrade fails for multi-channel Slack recipients over 100.
   ❌ Affected reports fail to send Slack notifications.
   ⚠️ Large orgs’ Slack alert migrations become brittle and error-prone.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Configure a Slack v1 report recipient whose 
`recipient_config_json["target"]` contains
   more than 100 comma-separated channels (permitted by 
`SlackNotification._get_channel` in
   `superset/reports/notifications/slack.py:30-39`, which joins 
`recipients_string_to_list`,
   and by `recipients_string_to_list` in `superset/utils/core.py:10-20` that 
splits an
   arbitrary-length comma/semicolon-separated string).
   
   2. Trigger the report execution so that `_send` in
   `superset/commands/report/execute.py:940-969` runs; when 
`should_use_v2_api()` returns
   True, `SlackNotification.send()` raises `SlackV1NotificationError` and 
`_send` catches it
   at `execute.py:26-32`, calling `self.update_report_schedule_slack_v2()` to 
auto-upgrade
   the Slack recipient.
   
   3. Inside `BaseReportState.update_report_schedule_slack_v2` in
   `superset/commands/report/execute.py:169-210`, the legacy target string 
(with all >100
   channels) is assigned to `channel_names` and passed directly to 
`get_channels_with_search`
   at `execute.py:190-197` with `exact_match=True` but without a `limit` 
argument, so the
   default `limit=100` from `get_channels_with_search` in 
`superset/utils/slack.py:172-178`
   is used.
   
   4. `_fetch_channels_with_search` in `superset/utils/slack.py:179-241` stops 
collecting
   matches once `len(matches) >= limit` (100) and returns at most 100 channels 
in
   `channels_data["result"]`; back in `update_report_schedule_slack_v2` at
   `execute.py:199-208`, `channels_list = 
recipients_string_to_list(channel_names)` contains
   all >100 targets, so `len(channels_list) != len(channels)` and the code 
raises
   `UpdateFailedError("Could not find the following channels: ...")`, causing 
the
   auto-upgrade (and thus the report send) to fail even though the extra 
channels exist in
   Slack but were never fetched due to the 100-result limit.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=553286d3a2a54937a60c0e6d564a47b5&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=553286d3a2a54937a60c0e6d564a47b5&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/commands/report/execute.py
   **Line:** 190:198
   **Comment:**
        *Api Mismatch: This migration lookup now calls a paginated API but 
never overrides the default page size, so it only inspects the first 100 
matching channels. If a legacy Slack recipient contains more than 100 channel 
targets, the upgrade will incorrectly report channels as missing and fail even 
though they exist. Pass an explicit limit based on the number of targets 
(and/or follow pagination until all requested exact matches are resolved).
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F42074&comment_hash=a6bacf818bd7e0d77f183d9fa3342da0898e34ac4034ec4e3c835e4e7c48083d&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F42074&comment_hash=a6bacf818bd7e0d77f183d9fa3342da0898e34ac4034ec4e3c835e4e7c48083d&reaction=dislike'>👎</a>



##########
superset/reports/api.py:
##########
@@ -682,14 +687,29 @@ def slack_channels(self, **kwargs: Any) -> Response:
             search_string = params.get("search_string")
             types = params.get("types", [])
             exact_match = params.get("exact_match", False)
+            cursor = params.get("cursor")
+            limit = params.get("limit", 100)
             force = params.get("force", False)
-            channels = get_channels_with_search(
+
+            # Clear cache if force refresh requested
+            if force:
+                cache_manager.cache.delete(SLACK_CHANNELS_CACHE_KEY)
+                
cache_manager.cache.delete(SLACK_CHANNELS_CONTINUATION_CURSOR_KEY)
+                logger.info("Slack channels cache cleared due to force=True")
+
+                # Trigger async cache warmup if caching is enabled
+                if current_app.config.get("SLACK_ENABLE_CACHING", True):
+                    cache_channels.apply_async(**get_cache_warmup_options())
+                    logger.info("Triggered async cache warmup task")
+

Review Comment:
   **Suggestion:** The forced refresh path directly dispatches a Celery task 
from the request, but this call can raise broker/transport exceptions when 
Celery is unavailable or misconfigured; those exceptions are not converted to a 
handled API response here, so the endpoint can fail with a 500 on `force=true`. 
Wrap task dispatch failures and continue serving channels (or return a 
controlled 422/503) instead of letting the request crash. [api mismatch]
   
   <details>
   <summary><b>Severity Level:</b> Major ⚠️</summary>
   
   ```mdx
   - ❌ Slack channel refresh fails with unhandled server error.
   - ⚠️ Alert Slack recipient dropdown breaks on cache refresh.
   - ⚠️ Admins must debug Celery errors from user actions.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Start Superset with this PR deployed, and configure alerts to use Slack 
v2; the backend
   Slack channels endpoint lives at `superset/reports/api.py:645-76` (the 
`slack_channels`
   method annotated with the OpenAPI docstring).
   
   2. In the Alerts & Reports UI, open the notification method editor for a 
Slack v2 alert;
   this uses `NotificationMethod` in
   
`superset-frontend/src/features/alerts/components/NotificationMethod.tsx:115-162`,
 which
   in turn calls `useSlackChannels` from
   `superset-frontend/src/features/alerts/hooks/useSlackChannels.ts:20-32`.
   
   3. Click the UI action that refreshes Slack channels (wired through
   `handleRefreshSlackChannels = refreshChannels` at 
`NotificationMethod.tsx:30`);
   `refreshChannels` in `useSlackChannels.ts:8-26` clears local caches and calls
   `fetchChannels({ search: '', page: 0, pageSize: 999, force: true })`, 
causing a GET to
   `/api/v1/report/slack_channels/?q=...` with `force=true` in the rison 
payload.
   
   4. The request reaches `slack_channels` in `superset/reports/api.py:46-76`, 
which enters
   the `if force:` block at lines 55-64, clears cache keys, and executes
   `cache_channels.apply_async(**get_cache_warmup_options())`. If the Celery 
broker or
   transport used by `celery_app` (imported from `superset.extensions` in this 
module) is
   unavailable or misconfigured, `apply_async` raises a Celery/Kombu exception 
that is not a
   `SupersetException`; since the try/except in `slack_channels` only catches
   `SupersetException` at line 74, this exception propagates and produces an 
HTTP 500 for the
   Slack channels endpoint instead of a controlled 4xx/5xx response.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=aaad66e9d4a249229ad1c83bc13dad29&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=aaad66e9d4a249229ad1c83bc13dad29&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/reports/api.py
   **Line:** 702:704
   **Comment:**
        *Api Mismatch: The forced refresh path directly dispatches a Celery 
task from the request, but this call can raise broker/transport exceptions when 
Celery is unavailable or misconfigured; those exceptions are not converted to a 
handled API response here, so the endpoint can fail with a 500 on `force=true`. 
Wrap task dispatch failures and continue serving channels (or return a 
controlled 422/503) instead of letting the request crash.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F42074&comment_hash=19feea15951f208a27145e365a9563a0c80505014d444d49c0431141a7258223&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F42074&comment_hash=19feea15951f208a27145e365a9563a0c80505014d444d49c0431141a7258223&reaction=dislike'>👎</a>



##########
superset/tasks/slack.py:
##########
@@ -15,29 +15,160 @@
 # specific language governing permissions and limitations
 # under the License.
 import logging
+from typing import Any, Optional
 
 from flask import current_app
 
-from superset.extensions import celery_app
-from superset.utils.slack import get_channels
+from superset.extensions import cache_manager, celery_app
+from superset.utils.slack import (
+    _fetch_from_api,
+    SLACK_CHANNELS_CACHE_KEY,
+    SLACK_CHANNELS_CONTINUATION_CURSOR_KEY,
+    SlackChannelTypes,
+)
 
 logger = logging.getLogger(__name__)
 
+# Fallback time limits used when the task is dispatched without explicit
+# options (e.g. a bare Celery beat schedule entry). On-demand callers should
+# pass config-driven limits via ``cache_channels.apply_async`` using
+# ``get_cache_warmup_options`` so that ``SLACK_CACHE_WARMUP_TIMEOUT`` is 
honored.
+DEFAULT_CACHE_WARMUP_TIME_LIMIT = 300  # 5 minutes
+DEFAULT_CACHE_WARMUP_SOFT_TIME_LIMIT = int(DEFAULT_CACHE_WARMUP_TIME_LIMIT * 
0.8)
 
-@celery_app.task(name="slack.cache_channels")
+
+def get_cache_warmup_options() -> dict[str, int]:
+    """
+    Build Celery ``apply_async`` options with config-driven time limits.
+
+    Read at dispatch time (inside an application context) rather than at import
+    so operator overrides of ``SLACK_CACHE_WARMUP_TIMEOUT`` are respected. The
+    soft limit is derived as 80% of the hard limit so the task can wind down
+    before Celery force-kills it.
+    """
+    time_limit = current_app.config.get(
+        "SLACK_CACHE_WARMUP_TIMEOUT", DEFAULT_CACHE_WARMUP_TIME_LIMIT
+    )
+    return {
+        "time_limit": time_limit,
+        "soft_time_limit": int(time_limit * 0.8),
+    }
+
+
+@celery_app.task(
+    name="slack.cache_channels",
+    time_limit=DEFAULT_CACHE_WARMUP_TIME_LIMIT,
+    soft_time_limit=DEFAULT_CACHE_WARMUP_SOFT_TIME_LIMIT,
+)
 def cache_channels() -> None:
+    """
+    Celery task to warm up the Slack channels cache.
+
+    This task fetches all Slack channels using pagination and stores them in 
cache.
+
+    Respects the following config variables:
+    - SLACK_ENABLE_CACHING: If False, this task does nothing
+    - SLACK_CACHE_TIMEOUT: How long to keep cached data
+    - SLACK_CACHE_MAX_CHANNELS: Maximum number of channels to cache. When the
+      workspace has more channels than this, the cache is truncated and a
+      continuation cursor is stored so paginated lookups can stream the
+      remainder from the Slack API.
+    """
+    enable_caching = current_app.config.get("SLACK_ENABLE_CACHING", True)
+    if not enable_caching:
+        logger.info(
+            "Slack caching disabled (SLACK_ENABLE_CACHING=False), skipping 
cache warmup"
+        )
+        return
+
     cache_timeout = current_app.config["SLACK_CACHE_TIMEOUT"]
+    max_channels = current_app.config.get("SLACK_CACHE_MAX_CHANNELS", 20000)
     retry_count = current_app.config.get("SLACK_API_RATE_LIMIT_RETRY_COUNT", 2)
 
     logger.info(
         "Starting Slack channels cache warm-up task "
-        "(cache_timeout=%ds, retry_count=%d)",
+        "(cache_timeout=%ds, max_channels=%d, retry_count=%d)",
         cache_timeout,
+        max_channels,
         retry_count,
     )
 
     try:
-        get_channels(force=True, cache_timeout=cache_timeout)
+        all_channels: list[Any] = []
+        cursor: Optional[str] = None
+        page_count = 0
+        # When the workspace exceeds ``max_channels`` we cache the first
+        # ``max_channels`` and remember the Slack cursor for the rest so that
+        # in-memory cache pagination can fall back to the API at the boundary.
+        continuation_cursor: Optional[str] = None
+
+        # Fetch directly from the Slack API (bypassing the cache-aware

Review Comment:
   **Suggestion:** The continuation cursor is taken from the page *after* the 
page that overflowed the cache cap, and then the overflowing page is truncated. 
When `SLACK_CACHE_MAX_CHANNELS` is not aligned to Slack page boundaries, 
channels between the cap and the next page cursor are permanently skipped. 
Store/handle in-page overflow (or resume from the current page with an offset) 
so no channels are dropped. [cache]
   
   <details>
   <summary><b>Severity Level:</b> Critical 🚨</summary>
   
   ```mdx
   - ❌ Some Slack channels never visible in recipient selector.
   - ⚠️ Large workspaces lose mid-range channels behind cache cap.
   - ⚠️ Users cannot target alerts to skipped Slack channels.
   ```
   </details>
   <details>
   <summary><b>Steps of Reproduction ✅ </b></summary>
   
   ```mdx
   1. Configure Superset with Slack caching enabled; the Celery task 
`cache_channels` is
   defined in `superset/tasks/slack.py:19-24` and uses 
`SLACK_CACHE_MAX_CHANNELS` (default
   20000) at line 45. Use a Slack workspace whose total channel count exceeds
   `SLACK_CACHE_MAX_CHANNELS`, which is exactly the large-workspace scenario 
this PR targets.
   
   2. Trigger the `slack.cache_channels` Celery task (either via periodic beat 
or via the
   backend `force` refresh path in `superset/reports/api.py:55-64`). Inside 
`cache_channels`,
   the loop at `superset/tasks/slack.py:69-103` repeatedly calls 
`_fetch_from_api` (imported
   from `superset/utils/slack.py`) with `limit=999`, appending 
`result["result"]` to
   `all_channels` at lines 80-82 and updating `cursor = 
result.get("next_cursor")` at line
   90.
   
   3. Once `len(all_channels) >= max_channels` while `result.get("has_more")` 
is still true,
   the branch at `superset/tasks/slack.py:95-103` executes: it sets 
`continuation_cursor =
   cursor` (the Slack `next_cursor` for the just-fetched page) and breaks the 
loop.
   Immediately afterward, `all_channels = all_channels[:max_channels]` at line 
105 truncates
   the combined list to the first `max_channels` entries, discarding the tail 
of the last
   fetched page (e.g., with `max_channels=20000`, only the first 20 entries 
from page 21 are
   cached and the remaining 979 are dropped).
   
   4. Later, when a user paginates Slack channels via the frontend hook 
`useSlackChannels`
   (`superset-frontend/src/features/alerts/hooks/useSlackChannels.ts:28-35`), 
the backend
   `get_channels_with_search` in `superset/utils/slack.py:13-38` first serves 
pages from
   cache using `_fetch_from_cache` (the in-memory paginator at
   `superset/utils/slack.py:19-61`). When the client advances past the cached 
boundary,
   `_fetch_from_cache` detects `offset >= len(channels)` at lines 27-35 and 
returns `None`,
   causing `get_channels_with_search` to fall back to the API using 
`continuation_cursor` at
   `superset/utils/slack.py:52-60` (either via the `"api:continue"` cursor path 
or the
   cache-boundary fallback at lines 2-20 in the 498-577 block). Because 
`continuation_cursor`
   points to the start of the *next* Slack page after the truncated one, the 
dropped tail of
   the last cached page is never re-fetched and those channels permanently 
disappear from
   both the cached pages and the API continuation path, so a slice of valid 
Slack channels
   cannot be listed or selected as alert recipients.
   ```
   </details>
   
   [![Fix in 
Cursor](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-cursor-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=a1c9a983ea114aa8bb46ebf81c6d2cbb&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
 [![Fix in VSCode 
Claude](https://new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/fix-in-vscode-claude-flat.svg)](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=a1c9a983ea114aa8bb46ebf81c6d2cbb&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
   
   *(Use Cmd/Ctrl + Click for best experience)*
   <details>
   <summary><b>Prompt for AI Agent 🤖 </b></summary>
   
   ```mdx
   This is a comment left during a code review.
   
   **Path:** superset/tasks/slack.py
   **Line:** 80:105
   **Comment:**
        *Cache: The continuation cursor is taken from the page *after* the page 
that overflowed the cache cap, and then the overflowing page is truncated. When 
`SLACK_CACHE_MAX_CHANNELS` is not aligned to Slack page boundaries, channels 
between the cap and the next page cursor are permanently skipped. Store/handle 
in-page overflow (or resume from the current page with an offset) so no 
channels are dropped.
   
   Validate the correctness of the flagged issue. If correct, How can I resolve 
this? If you propose a fix, implement it and please make it concise.
   Once fix is implemented, also check other comments on the same PR, and ask 
user if the user wants to fix the rest of the comments as well. if said yes, 
then fetch all the comments validate the correctness and implement a minimal fix
   ```
   </details>
   <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F42074&comment_hash=d652913085f80490a1b3993c6cf5f38c56408fe1404088c9279b4e5d4c2a799f&reaction=like'>👍</a>
 | <a 
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F42074&comment_hash=d652913085f80490a1b3993c6cf5f38c56408fe1404088c9279b4e5d4c2a799f&reaction=dislike'>👎</a>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to