Skip to content

Conversation

@Kalra-V
Copy link
Contributor

@Kalra-V Kalra-V commented Oct 26, 2025

  • Add DELETE /graph/{graph_name}/triggers endpoint

  • Implement cancel_triggers controller with support for PENDING and TRIGGERING statuses

  • Add mark_as_cancelled function to update trigger status and set expires_at

  • Include CANCELLED status in TTL index for automatic cleanup

  • Return count and status message in response

  • Add test_cancel_triggers.py with 9 controller tests covering:

    • Cancellation of PENDING and TRIGGERING triggers
    • No triggers found scenario
    • Settings integration and retention hours
    • Error handling for database and mark errors
    • Batch processing multiple triggers
  • Update test_trigger_cron.py to include mark_as_cancelled in
    parametrized tests for consistency with mark_as_triggered and
    mark_as_failed

API endpoint response when cancelling 1 trigger:

Screenshot 2025-10-26 at 6 09 12 PM

API endpoint response when no PENDING or TRIGGERING trigger is there:

Screenshot 2025-10-26 at 6 09 25 PM

Vansh Kalra added 2 commits October 26, 2025 18:07
- Add DELETE /graph/{graph_name}/triggers endpoint
- Implement cancel_triggers controller with support for PENDING and TRIGGERING statuses
- Add mark_as_cancelled function to update trigger status and set expires_at
- Include CANCELLED status in TTL index for automatic cleanup
- Return count and status message in response
- Add test_cancel_triggers.py with 9 controller tests covering:
  - Cancellation of PENDING and TRIGGERING triggers
  - No triggers found scenario
  - Settings integration and retention hours
  - Error handling for database and mark errors
  - Batch processing multiple triggers

- Update test_trigger_cron.py to include mark_as_cancelled in
  parametrized tests for consistency with mark_as_triggered and
  mark_as_failed
@safedep
Copy link

safedep bot commented Oct 26, 2025

SafeDep Report Summary

Green Malicious Packages Badge Green Vulnerable Packages Badge Green Risky License Badge

No dependency changes detected. Nothing to scan.

This report is generated by SafeDep Github App

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 26, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added new API endpoint to cancel active triggers that are pending or in-progress within a specified graph
    • All cancelled triggers are automatically scheduled for deletion per configured retention policy settings
    • Each request returns confirmation with the total count of cancelled triggers and operation status

Walkthrough

Adds a DELETE API endpoint to cancel PENDING/TRIGGERING triggers for a graph, a controller that queries and concurrently marks found triggers as CANCELLED using retention hours, a response model CancelTriggerResponse, a mark_as_cancelled helper, TTL index extended to include CANCELLED, and unit tests for these flows.

Changes

Cohort / File(s) Summary
Route / API
state-manager/app/routes.py
New DELETE /graph/{graph_name}/triggers route cancel_triggers_route with API key dependency that delegates to the controller and returns CancelTriggerResponse.
Controller
state-manager/app/controller/cancel_triggers.py
New async cancel_triggers(namespace_name: str, graph_name: str, x_exosphere_request_id: str) -> CancelTriggerResponse that queries triggers with statuses PENDING or TRIGGERING, returns zero when none found, fetches trigger_retention_hours, concurrently calls mark_as_cancelled for each trigger, logs and returns cancelled count, and re-raises exceptions.
Models
state-manager/app/models/cancel_trigger_models.py, state-manager/app/models/db/trigger.py
Added CancelTriggerResponse Pydantic model (namespace, graph_name, cancelled_count, message); extended TTL index partialFilterExpression to include TriggerStatusEnum.CANCELLED for expires_at.
Task / Helper
state-manager/app/tasks/trigger_cron.py
New async mark_as_cancelled(trigger: DatabaseTriggers, retention_hours: int) that sets trigger_status = CANCELLED and computes/updates expires_at based on retention hours (UTC).
Tests
state-manager/tests/unit/controller/test_cancel_triggers.py, state-manager/tests/unit/tasks/test_trigger_cron.py, state-manager/tests/unit/test_routes.py
Added/updated unit tests covering successful cancellations, zero-results, retention-hours usage, TTL behavior including CANCELLED, batching/concurrency, error propagation, and route/model exposure.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API as "DELETE /graph/{graph_name}/triggers"
    participant Route as cancel_triggers_route
    participant Controller as cancel_triggers()
    participant DB as DatabaseTriggers
    participant Settings as SettingsService
    participant Task as mark_as_cancelled()

    Client->>API: DELETE + API key
    API->>Route: forward request
    Route->>Controller: namespace, graph, request_id
    Controller->>DB: query triggers where status IN [PENDING, TRIGGERING]
    DB-->>Controller: list of triggers
    alt triggers found
        Controller->>Settings: get_settings() -> trigger_retention_hours
        Settings-->>Controller: retention_hours
        Controller->>Task: asyncio.gather mark_as_cancelled(trigger, retention_hours) for each
        Task->>DB: update trigger (status=CANCELLED, expires_at)
        DB-->>Task: update results
    else no triggers
        Controller-->>Route: CancelTriggerResponse(cancelled_count=0, message)
    end
    Controller-->>Route: CancelTriggerResponse(cancelled_count=n, message)
    Route-->>API: response 200
    API-->>Client: returns response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review controller DB query filters and correct status set (PENDING, TRIGGERING).
  • Verify concurrent cancellation via asyncio.gather and exception handling semantics.
  • Validate retention-hours retrieval and UTC expires_at computation in mark_as_cancelled.
  • Confirm TTL index change to include CANCELLED aligns with retention strategy.
  • Inspect new unit tests for adequate mocking and coverage of edge/error cases.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • NiveditJain

Poem

🐇 I hopped through logs with carrot-bright cheer,

I nudged pending things to vanish clear,
Marked them cancelled, set their timed goodbye,
A tiny hop — the stale queues sigh,
Now fields are tidy, and I munch a cheer.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "Enhancement/cancel triggers" directly describes the main objective of the changeset, which is to add a new feature for canceling triggers. The title clearly summarizes the primary change without vagueness or off-topic content. The changeset implements a complete feature including a new DELETE endpoint, controller logic, supporting models, database updates, and comprehensive tests—all focused on trigger cancellation, which aligns precisely with what the title conveys.
Description Check ✅ Passed The pull request description is clearly and comprehensively related to the changeset. It details the new DELETE endpoint, the cancel_triggers controller implementation with status filtering, the mark_as_cancelled function, TTL index updates, and the response structure. Additionally, it describes the test coverage added (9 controller tests with specific scenarios) and updates to existing tests. The inclusion of screenshots demonstrating the API behavior provides concrete evidence of the implementation, making the description directly relevant to all major changes in the pull request.
Docstring Coverage ✅ Passed Docstring coverage is 84.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Kalra-V, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement by providing the capability to cancel active graph triggers. It establishes a new API endpoint for users to programmatically halt graph executions that are currently in a PENDING or TRIGGERING state. The changes encompass the development of a dedicated controller for handling cancellation requests, a function to mark triggers as cancelled with an expiration timestamp, and updates to the database schema to facilitate automatic cleanup of these cancelled triggers. The new functionality is thoroughly validated with comprehensive unit tests.

Highlights

  • New API Endpoint for Trigger Cancellation: Introduced a new DELETE /graph/{graph_name}/triggers API endpoint, allowing users to cancel pending or triggering graph executions for a specific graph and namespace.
  • Cancellation Logic Implementation: Implemented a new cancel_triggers controller that identifies triggers in PENDING or TRIGGERING states and processes their cancellation. A mark_as_cancelled function was added to update trigger status and set an expires_at timestamp.
  • Automatic Cleanup for Cancelled Triggers: The CANCELLED status has been integrated into the existing TTL (Time-To-Live) index for DatabaseTriggers, ensuring that cancelled triggers are automatically removed from the database after a configurable retention period.
  • Comprehensive Unit Testing: Added a new test file test_cancel_triggers.py with 9 unit tests covering various scenarios, including successful cancellation, cases with no triggers, correct query filtering, usage of retention hour settings, and error handling. Existing tests in test_trigger_cron.py were also updated to include mark_as_cancelled.
  • Enhanced API Response: The API response for trigger cancellation now includes the count of triggers successfully cancelled and a descriptive status message, providing clear feedback to the user.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai coderabbitai bot added the enhancement New feature or request label Oct 26, 2025
@codecov
Copy link

codecov bot commented Oct 26, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new endpoint to cancel pending and triggering triggers, along with corresponding controller logic, database model updates, and comprehensive tests. The implementation is solid and well-tested. My feedback focuses on improving performance by using concurrent execution for cancellations, enhancing code clarity by removing unused imports and duplicated logic, and strengthening the tests with more specific assertions. Overall, this is a great enhancement.

Comment on lines +138 to +140
# Check that all three conditions are in the call
# The call should include namespace, graph_name, and In for trigger_status
assert call_args is not None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The assertion assert call_args is not None is quite weak. It only confirms that the find method was called, but not with which arguments. To make this test more robust, you should assert on the specific filters being passed to the find method. A simple but effective improvement is to check the number of filter arguments passed.

Suggested change
# Check that all three conditions are in the call
# The call should include namespace, graph_name, and In for trigger_status
assert call_args is not None
# Verify find was called with the correct number of filter arguments
# (namespace, graph_name, and trigger_status).
assert len(call_args.args) == 3

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06ba9f3 and 4c478eb.

📒 Files selected for processing (7)
  • state-manager/app/controller/cancel_triggers.py (1 hunks)
  • state-manager/app/models/cancel_trigger_models.py (1 hunks)
  • state-manager/app/models/db/trigger.py (1 hunks)
  • state-manager/app/routes.py (2 hunks)
  • state-manager/app/tasks/trigger_cron.py (1 hunks)
  • state-manager/tests/unit/controller/test_cancel_triggers.py (1 hunks)
  • state-manager/tests/unit/tasks/test_trigger_cron.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
state-manager/tests/unit/controller/test_cancel_triggers.py (4)
state-manager/app/controller/cancel_triggers.py (1)
  • cancel_triggers (15-67)
state-manager/app/models/cancel_trigger_models.py (1)
  • CancelTriggerResponse (4-8)
state-manager/app/models/db/trigger.py (1)
  • DatabaseTriggers (9-53)
state-manager/app/models/trigger_models.py (1)
  • TriggerStatusEnum (9-14)
state-manager/app/controller/cancel_triggers.py (6)
state-manager/app/models/cancel_trigger_models.py (1)
  • CancelTriggerResponse (4-8)
state-manager/app/models/db/trigger.py (1)
  • DatabaseTriggers (9-53)
state-manager/app/models/trigger_models.py (1)
  • TriggerStatusEnum (9-14)
state-manager/app/singletons/logs_manager.py (2)
  • LogsManager (9-66)
  • get_logger (65-66)
state-manager/app/config/settings.py (1)
  • get_settings (34-38)
state-manager/app/tasks/trigger_cron.py (2)
  • trigger_cron (97-101)
  • mark_as_cancelled (103-112)
state-manager/app/routes.py (3)
state-manager/app/models/cancel_trigger_models.py (1)
  • CancelTriggerResponse (4-8)
state-manager/app/controller/cancel_triggers.py (1)
  • cancel_triggers (15-67)
state-manager/app/utils/check_secret.py (1)
  • check_api_key (11-16)
state-manager/tests/unit/tasks/test_trigger_cron.py (2)
state-manager/app/tasks/trigger_cron.py (1)
  • mark_as_cancelled (103-112)
state-manager/app/models/trigger_models.py (1)
  • TriggerStatusEnum (9-14)
state-manager/app/tasks/trigger_cron.py (2)
state-manager/app/models/db/trigger.py (1)
  • DatabaseTriggers (9-53)
state-manager/app/models/trigger_models.py (1)
  • TriggerStatusEnum (9-14)
state-manager/app/models/db/trigger.py (1)
state-manager/app/models/trigger_models.py (1)
  • TriggerStatusEnum (9-14)
🔇 Additional comments (11)
state-manager/app/models/cancel_trigger_models.py (1)

1-9: LGTM!

The CancelTriggerResponse model is well-defined with clear field descriptions. The structure aligns with the controller implementation and follows Pydantic best practices.

state-manager/app/models/db/trigger.py (1)

47-48: LGTM!

The TTL index update correctly includes CANCELLED status alongside TRIGGERED and FAILED. This ensures that cancelled triggers are automatically cleaned up when their expires_at timestamp is reached, maintaining consistency with other terminal trigger states.

state-manager/tests/unit/tasks/test_trigger_cron.py (3)

13-13: LGTM!

The import of mark_as_cancelled correctly extends the module's public interface for testing.


25-29: LGTM!

The parametrized test now includes mark_as_cancelled alongside mark_as_triggered and mark_as_failed, ensuring consistent behavior across all marking functions for TTL and retention logic.


67-77: LGTM!

The test coverage for custom retention periods now includes mark_as_cancelled with various retention hour values (12, 24, 48), ensuring the function respects custom retention periods consistently with other mark functions.

state-manager/app/tasks/trigger_cron.py (2)

103-112: LGTM!

The mark_as_cancelled function correctly follows the same pattern as mark_as_triggered and mark_as_failed. It properly:

  • Calculates expires_at in UTC using the provided retention_hours
  • Updates the trigger status to CANCELLED
  • Sets the expires_at field for TTL-based cleanup

101-101: The concurrent execution with asyncio.gather is pre-existing code, not introduced by this PR.

Based on the git history, the asyncio.gather line at line 101 was already present in previous commits and is not part of the changes in this PR. The only modifications to trigger_cron.py in the last 5 commits are: (1) a newline formatting change at line 101, and (2) the addition of the mark_as_cancelled function. This concurrent execution pattern was established several commits ago and is already tested by test_trigger_cron(), which explicitly verifies that handle_trigger is called trigger_workers times concurrently.

Likely an incorrect or invalid review comment.

state-manager/app/controller/cancel_triggers.py (1)

27-67: Verify partial cancellation behavior on error.

If mark_as_cancelled fails partway through the loop (e.g., on the 3rd of 5 triggers), some triggers will be marked as cancelled while others remain in their original state. The exception is re-raised (Line 67), but there's no rollback mechanism.

Confirm this partial cancellation behavior is acceptable, or consider implementing error recovery logic.

state-manager/tests/unit/controller/test_cancel_triggers.py (1)

1-275: LGTM!

The test suite provides comprehensive coverage for the cancel_triggers controller:

  • Successful cancellation scenarios for PENDING and TRIGGERING statuses
  • Empty result handling
  • Query filter validation
  • Settings integration
  • Error handling for both database and marking failures
  • Batch processing verification

The tests are well-structured, use appropriate mocking, and follow pytest best practices.

state-manager/app/routes.py (2)

57-60: LGTM!

The import statements are well-organized with clear comments, following the existing pattern in the file.


244-261: LGTM!

The new DELETE /graph/{graph_name}/triggers endpoint follows the same implementation pattern as other secured endpoints in this file:

  • Proper response model configuration
  • Consistent API key validation
  • Request ID extraction and passing
  • Clean delegation to the controller

- Made changes that coderabbitai & gemini required
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
state-manager/tests/unit/controller/test_cancel_triggers.py (1)

137-139: Strengthen assertion to verify query filters.

The assertion only checks that find was called but doesn't verify the query filters (namespace, graph_name, trigger_status). This was flagged in a previous review and remains unaddressed.

Apply this diff to verify the filter arguments:

         # Verify find was called with correct arguments
         mock_db.find.assert_called_once()
         call_args = mock_db.find.call_args
         
-        # Check that all three conditions are in the call
-        # The call should include namespace, graph_name, and In for trigger_status
-        assert call_args is not None
+        # Verify that find was called with three filter arguments:
+        # namespace, graph_name, and trigger_status (In operator)
+        assert len(call_args.args) == 3
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c478eb and 14097f6.

📒 Files selected for processing (2)
  • state-manager/app/controller/cancel_triggers.py (1 hunks)
  • state-manager/tests/unit/controller/test_cancel_triggers.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
state-manager/tests/unit/controller/test_cancel_triggers.py (3)
state-manager/app/controller/cancel_triggers.py (1)
  • cancel_triggers (15-67)
state-manager/app/models/cancel_trigger_models.py (1)
  • CancelTriggerResponse (4-8)
state-manager/app/models/db/trigger.py (1)
  • DatabaseTriggers (9-53)
state-manager/app/controller/cancel_triggers.py (6)
state-manager/app/models/cancel_trigger_models.py (1)
  • CancelTriggerResponse (4-8)
state-manager/app/models/db/trigger.py (1)
  • DatabaseTriggers (9-53)
state-manager/app/models/trigger_models.py (1)
  • TriggerStatusEnum (9-14)
state-manager/app/singletons/logs_manager.py (2)
  • LogsManager (9-66)
  • get_logger (65-66)
state-manager/app/config/settings.py (1)
  • get_settings (34-38)
state-manager/app/tasks/trigger_cron.py (2)
  • trigger_cron (97-101)
  • mark_as_cancelled (103-112)
🔇 Additional comments (2)
state-manager/app/controller/cancel_triggers.py (1)

1-67: LGTM! Past review suggestions have been addressed.

The controller correctly implements concurrent cancellation using asyncio.gather and simplifies the counter logic with len(triggers). The structure, error handling, and integration with settings and mark_as_cancelled are all sound.

state-manager/tests/unit/controller/test_cancel_triggers.py (1)

1-274: Comprehensive test coverage.

The test suite covers all major scenarios including success cases, error handling, settings integration, and edge cases. Well-structured and thorough.

Kalra-V and others added 2 commits October 26, 2025 18:40
- Fixed naming convent suggested by coderabbitai

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
state-manager/tests/unit/controller/test_cancel_triggers.py (1)

137-139: The assertion is too weak and doesn't verify the actual filter conditions.

This assertion only confirms that find was called, but doesn't validate the namespace, graph_name, or trigger_status filters. This issue was previously flagged.

Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14097f6 and 569e31f.

📒 Files selected for processing (1)
  • state-manager/tests/unit/controller/test_cancel_triggers.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
state-manager/tests/unit/controller/test_cancel_triggers.py (3)
state-manager/app/controller/cancel_triggers.py (1)
  • cancel_triggers (15-67)
state-manager/app/models/cancel_trigger_models.py (1)
  • CancelTriggerResponse (4-8)
state-manager/app/models/db/trigger.py (1)
  • DatabaseTriggers (9-53)
🔇 Additional comments (6)
state-manager/tests/unit/controller/test_cancel_triggers.py (6)

53-87: LGTM!

This test properly verifies that mark_as_cancelled is called with the correct trigger and retention_hours arguments.


89-110: LGTM!

This test correctly verifies the no-triggers-found path, including the early return that skips calling get_settings and mark_as_cancelled.


142-168: LGTM!

This test properly verifies that the retention_hours from settings (48) is correctly passed to mark_as_cancelled.


170-182: LGTM!

This test correctly verifies that database errors are propagated to the caller.


184-210: LGTM!

This test correctly verifies that errors during mark_as_cancelled are propagated. Since the controller uses asyncio.gather, exceptions will bubble up appropriately.


246-273: LGTM!

This test correctly verifies that get_settings is called when triggers exist and that the retention_hours from settings are properly passed to mark_as_cancelled.

Comment on lines +49 to +50
# Verify mark_as_cancelled was called for each trigger
assert mock_mark_cancelled.call_count == 2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Verify the arguments passed to mark_as_cancelled, not just the call count.

The test confirms mark_as_cancelled was called twice but doesn't verify that each trigger was passed with the correct retention_hours (24). This could miss bugs where incorrect arguments are used.

Consider adding assertions to verify the arguments:

-        # Verify mark_as_cancelled was called for each trigger
-        assert mock_mark_cancelled.call_count == 2
+        # Verify mark_as_cancelled was called for each trigger with correct retention_hours
+        assert mock_mark_cancelled.call_count == 2
+        mock_mark_cancelled.assert_any_call(mock_trigger1, 24)
+        mock_mark_cancelled.assert_any_call(mock_trigger2, 24)
📝 Committable suggestion

‼️ 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.

Suggested change
# Verify mark_as_cancelled was called for each trigger
assert mock_mark_cancelled.call_count == 2
# Verify mark_as_cancelled was called for each trigger with correct retention_hours
assert mock_mark_cancelled.call_count == 2
mock_mark_cancelled.assert_any_call(mock_trigger1, 24)
mock_mark_cancelled.assert_any_call(mock_trigger2, 24)
🤖 Prompt for AI Agents
state-manager/tests/unit/controller/test_cancel_triggers.py around lines 49-50:
the test only asserts mock_mark_cancelled.call_count == 2 but doesn't verify the
actual arguments; update the test to assert that mark_as_cancelled was called
with each trigger and retention_hours=24 by checking
mock_mark_cancelled.call_args_list or using assert_has_calls with two expected
calls (trigger1, retention_hours=24) and (trigger2, retention_hours=24) in the
correct order (or unordered if order is not guaranteed).

Comment on lines +213 to +245
async def test_cancel_triggers_multiple_triggers_batch():
"""Test that multiple triggers are cancelled in batch"""
namespace_name = "test_namespace"
graph_name = "test_graph"
x_exosphere_request_id = "test_request_id"

mock_trigger1 = MagicMock(spec=DatabaseTriggers)
mock_trigger1.id = "trigger_id_1"
mock_trigger2 = MagicMock(spec=DatabaseTriggers)
mock_trigger2.id = "trigger_id_2"
mock_trigger3 = MagicMock(spec=DatabaseTriggers)
mock_trigger3.id = "trigger_id_3"

with patch('app.controller.cancel_triggers.DatabaseTriggers') as mock_db, \
patch('app.controller.cancel_triggers.get_settings') as mock_get_settings, \
patch('app.controller.cancel_triggers.mark_as_cancelled') as mock_mark_cancelled:

mock_query = MagicMock()
mock_query.to_list = AsyncMock(return_value=[mock_trigger1, mock_trigger2, mock_trigger3])
mock_db.find.return_value = mock_query

mock_settings = MagicMock()
mock_settings.trigger_retention_hours = 24
mock_get_settings.return_value = mock_settings

result = await cancel_triggers(namespace_name, graph_name, x_exosphere_request_id)

# Verify correct count
assert result.cancelled_count == 3

# Verify all triggers were processed
assert mock_mark_cancelled.call_count == 3

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Test name improvement looks good, but consider verifying arguments.

The renamed test correctly reflects batch processing without implying ordering guarantees. However, similar to the first test, it only verifies the call count without checking that each trigger is passed with the correct retention_hours.

Consider adding assertions to verify the arguments:

         # Verify all triggers were processed
         assert mock_mark_cancelled.call_count == 3
+        mock_mark_cancelled.assert_any_call(mock_trigger1, 24)
+        mock_mark_cancelled.assert_any_call(mock_trigger2, 24)
+        mock_mark_cancelled.assert_any_call(mock_trigger3, 24)
📝 Committable suggestion

‼️ 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.

Suggested change
async def test_cancel_triggers_multiple_triggers_batch():
"""Test that multiple triggers are cancelled in batch"""
namespace_name = "test_namespace"
graph_name = "test_graph"
x_exosphere_request_id = "test_request_id"
mock_trigger1 = MagicMock(spec=DatabaseTriggers)
mock_trigger1.id = "trigger_id_1"
mock_trigger2 = MagicMock(spec=DatabaseTriggers)
mock_trigger2.id = "trigger_id_2"
mock_trigger3 = MagicMock(spec=DatabaseTriggers)
mock_trigger3.id = "trigger_id_3"
with patch('app.controller.cancel_triggers.DatabaseTriggers') as mock_db, \
patch('app.controller.cancel_triggers.get_settings') as mock_get_settings, \
patch('app.controller.cancel_triggers.mark_as_cancelled') as mock_mark_cancelled:
mock_query = MagicMock()
mock_query.to_list = AsyncMock(return_value=[mock_trigger1, mock_trigger2, mock_trigger3])
mock_db.find.return_value = mock_query
mock_settings = MagicMock()
mock_settings.trigger_retention_hours = 24
mock_get_settings.return_value = mock_settings
result = await cancel_triggers(namespace_name, graph_name, x_exosphere_request_id)
# Verify correct count
assert result.cancelled_count == 3
# Verify all triggers were processed
assert mock_mark_cancelled.call_count == 3
async def test_cancel_triggers_multiple_triggers_batch():
"""Test that multiple triggers are cancelled in batch"""
namespace_name = "test_namespace"
graph_name = "test_graph"
x_exosphere_request_id = "test_request_id"
mock_trigger1 = MagicMock(spec=DatabaseTriggers)
mock_trigger1.id = "trigger_id_1"
mock_trigger2 = MagicMock(spec=DatabaseTriggers)
mock_trigger2.id = "trigger_id_2"
mock_trigger3 = MagicMock(spec=DatabaseTriggers)
mock_trigger3.id = "trigger_id_3"
with patch('app.controller.cancel_triggers.DatabaseTriggers') as mock_db, \
patch('app.controller.cancel_triggers.get_settings') as mock_get_settings, \
patch('app.controller.cancel_triggers.mark_as_cancelled') as mock_mark_cancelled:
mock_query = MagicMock()
mock_query.to_list = AsyncMock(return_value=[mock_trigger1, mock_trigger2, mock_trigger3])
mock_db.find.return_value = mock_query
mock_settings = MagicMock()
mock_settings.trigger_retention_hours = 24
mock_get_settings.return_value = mock_settings
result = await cancel_triggers(namespace_name, graph_name, x_exosphere_request_id)
# Verify correct count
assert result.cancelled_count == 3
# Verify all triggers were processed
assert mock_mark_cancelled.call_count == 3
mock_mark_cancelled.assert_any_call(mock_trigger1, 24)
mock_mark_cancelled.assert_any_call(mock_trigger2, 24)
mock_mark_cancelled.assert_any_call(mock_trigger3, 24)
🤖 Prompt for AI Agents
In state-manager/tests/unit/controller/test_cancel_triggers.py around lines 213
to 245, the test checks only that mark_as_cancelled was called three times but
does not verify the actual arguments; update the test to assert that each
mark_as_cancelled call includes the correct trigger instance and the
retention_hours pulled from mock_get_settings
(mock_settings.trigger_retention_hours) and that the x_exosphere_request_id is
forwarded; do this by inspecting mock_mark_cancelled.call_args_list and either
asserting each expected call (using unittest.mock.call with the trigger and
keyword retention_hours=mock_settings.trigger_retention_hours and
x_exosphere_request_id) is present or by iterating over call_args_list to check
trigger identity and the expected keyword values, without relying on call order.

@NiveditJain
Copy link
Member

@Kalra-V requesting to add apt tests for the code to increase the coverage.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 569e31f and 03c460f.

📒 Files selected for processing (2)
  • state-manager/app/controller/cancel_triggers.py (1 hunks)
  • state-manager/tests/unit/test_routes.py (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
state-manager/app/controller/cancel_triggers.py (6)
state-manager/app/models/cancel_trigger_models.py (1)
  • CancelTriggerResponse (4-8)
state-manager/app/models/db/trigger.py (1)
  • DatabaseTriggers (9-53)
state-manager/app/models/trigger_models.py (1)
  • TriggerStatusEnum (9-14)
state-manager/app/singletons/logs_manager.py (2)
  • LogsManager (9-66)
  • get_logger (65-66)
state-manager/app/config/settings.py (1)
  • get_settings (34-38)
state-manager/app/tasks/trigger_cron.py (2)
  • trigger_cron (97-101)
  • mark_as_cancelled (103-112)
state-manager/tests/unit/test_routes.py (2)
state-manager/app/models/cancel_trigger_models.py (1)
  • CancelTriggerResponse (4-8)
state-manager/app/routes.py (2)
  • manual_retry_state_route (194-203)
  • cancel_triggers_route (251-260)
🔇 Additional comments (8)
state-manager/tests/unit/test_routes.py (5)

12-12: LGTM!

The import of CancelTriggerResponse is necessary for the new test cases and follows the existing import structure.


41-41: LGTM!

The route path assertion correctly validates the presence of the new cancel triggers endpoint.


300-329: LGTM!

The model validation tests comprehensively cover both successful cancellation scenarios and the zero-count edge case. The tests follow the established patterns in this file.


353-373: LGTM!

The handler existence checks properly validate that cancel_triggers_route is defined and callable, consistent with checks for other route handlers.


1156-1221: LGTM!

The three test functions comprehensively cover the cancel triggers route:

  • Valid API key scenario with proper controller invocation
  • Invalid API key scenario with expected 401 response
  • Missing request ID scenario with UUID generation

The tests follow the established patterns and properly mock dependencies.

state-manager/app/controller/cancel_triggers.py (3)

1-13: LGTM!

All imports are used and necessary. The asyncio import supports concurrent cancellation, and the logger initialization follows the standard pattern.


27-62: LGTM! Concurrent cancellation properly implemented.

The controller correctly:

  • Queries for PENDING/TRIGGERING triggers with proper filtering
  • Returns early with a clear message when no triggers are found
  • Cancels triggers concurrently using asyncio.gather (addressing past feedback)
  • Uses len(triggers) directly for the count (addressing past feedback)
  • Logs outcomes at appropriate levels

The concurrent implementation significantly improves performance when cancelling multiple triggers.


64-67: LGTM!

Error handling is appropriate: logging provides context for troubleshooting, and re-raising allows the route handler to properly format HTTP error responses.

Comment on lines +50 to +53
# Cancel each trigger concurrently
cancelled_count = len(triggers)
cancellation_tasks = [mark_as_cancelled(trigger, retention_hours) for trigger in triggers]
await asyncio.gather(*cancellation_tasks)
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Optional: Consider partial-failure visibility.

The current asyncio.gather uses fail-fast behavior: if any mark_as_cancelled call fails, the entire batch fails and the user receives no indication of how many triggers were successfully cancelled before the failure.

For better user experience, consider using return_exceptions=True to handle partial failures:

# Cancel each trigger concurrently
cancellation_tasks = [mark_as_cancelled(trigger, retention_hours) for trigger in triggers]
results = await asyncio.gather(*cancellation_tasks, return_exceptions=True)

# Count successful cancellations
cancelled_count = sum(1 for r in results if not isinstance(r, Exception))
failures = [r for r in results if isinstance(r, Exception)]

if failures:
    logger.warning(f"Failed to cancel {len(failures)} trigger(s) for graph {graph_name}", 
                   x_exosphere_request_id=x_exosphere_request_id)
    # Optionally: raise if no triggers were cancelled successfully

The current all-or-nothing approach is acceptable for ensuring consistency, but partial success information may be valuable for troubleshooting.

🤖 Prompt for AI Agents
In state-manager/app/controller/cancel_triggers.py around lines 50 to 53, the
code uses asyncio.gather without return_exceptions so a single failure aborts
the whole batch and hides partial successes; change to await
asyncio.gather(*tasks, return_exceptions=True), collect results, count
non-Exception results as cancelled_count, and log/collect Exception instances as
failures (warning with graph_name and request id), optionally raising only if
zero succeeded.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kalra-V this suggestion seems valid. Lets incorporate this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

triggers = await DatabaseTriggers.find(
DatabaseTriggers.namespace == namespace_name,
DatabaseTriggers.graph_name == graph_name,
In(DatabaseTriggers.trigger_status, [TriggerStatusEnum.PENDING, TriggerStatusEnum.TRIGGERING])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a Database trigger is in TRIGGERING state, I don't think we should cancel in that state. Why? Because it might be immediately picked by the process to move to TRIGGERED. This will create an inconsistent state where actually the job is Triggered but it shows TRIGGERING.

Comment on lines +46 to +48
# Get retention hours from settings
settings = get_settings()
retention_hours = settings.trigger_retention_hours
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this here?

# Cancel each trigger concurrently
cancelled_count = len(triggers)
cancellation_tasks = [mark_as_cancelled(trigger, retention_hours) for trigger in triggers]
await asyncio.gather(*cancellation_tasks)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this sends a number of queries, can we use some bulk update operation here? Also can we merge and do something like bulk get and update.

Comment on lines +50 to +53
# Cancel each trigger concurrently
cancelled_count = len(triggers)
cancellation_tasks = [mark_as_cancelled(trigger, retention_hours) for trigger in triggers]
await asyncio.gather(*cancellation_tasks)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kalra-V this suggestion seems valid. Lets incorporate this.

from app.models.trigger_models import TriggerStatusEnum
from app.singletons.logs_manager import LogsManager
from app.config.settings import get_settings
from app.tasks.trigger_cron import mark_as_cancelled
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will not recommend importing from tasks in controller. This might lead to confusing dependencies going forward.

Comment on lines +103 to +112
async def mark_as_cancelled(trigger: DatabaseTriggers, retention_hours: int):
expires_at = datetime.now(timezone.utc) + timedelta(hours=retention_hours)

await DatabaseTriggers.get_pymongo_collection().update_one(
{"_id": trigger.id},
{"$set": {
"trigger_status": TriggerStatusEnum.CANCELLED,
"expires_at": expires_at
}}
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants