-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Enhancement: Add feature to manually retry failed task #1417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
@mher anything I can do to help get this merged? |
sc68cal
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concerns have been addressed, thank you for this contribution!
|
@sc68cal how can i get this merged? |
I am not the maintainer, the maintainer needs to approve and merge |
marcus-campos
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
|
Thanks for the feature, @mher could we get this merged? |
|
Hi! Thanks for adding this awesome feature @yashpapa6969 ! I tested it locally and noticed a small issue: I think we need to add a route for the new controller in (r"/api/task/reapply/(.+)", tasks.TaskReapply),Otherwise the Retry button will fail with 404 |
|
@matias-martini thank you updated |
|
@yashpapa6969 Thanks for the updates! 🙌 Would you mind adding a test for the new API endpoint? It'll help ensure we cover the most common cases. |
|
@yashpapa6969 Thanks for the effort, can we have the tests to merge this? |
|
Any update on this? |
|
Would be great to see this feature merged ! |
auvipy
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will also need unit tests to avoid regression
|
@yashpapa6969, thank you so much for the effort! Could you help us with the tests for it so we can review and merge? |
|
hello, any updates on this? thanks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a new feature to manually retry failed tasks through the Flower UI and API. It introduces a /api/task/reapply/{taskid} endpoint that retrieves a failed task's original arguments and reapplies it as a new task.
Key Changes:
- Added
TaskReapplyAPI endpoint to reapply tasks with their original arguments - Implemented utility functions (
parse_args,parse_kwargs,make_json_serializable) to handle argument parsing and JSON serialization - Added a "Retry" button in the UI for tasks in FAILURE state
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| flower/api/tasks.py | Implements the new TaskReapply endpoint handler with error handling and task reapplication logic |
| flower/utils/tasks.py | Adds helper functions for parsing task arguments from various formats (JSON, Python literals) and ensuring JSON serializability |
| flower/urls.py | Registers the new /api/task/reapply endpoint route |
| flower/templates/task.html | Adds a "Retry" button for failed tasks in the task detail page |
| flower/static/js/flower.js | Implements client-side logic to handle retry button clicks and API communication |
| tests/unit/api/test_tasks.py | Adds comprehensive test coverage for the TaskReapply endpoint covering success, error cases, and edge cases |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if args.startswith('(') and args.endswith(')'): | ||
| return ast.literal_eval(args) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security concern: Using ast.literal_eval on user-provided input (task.args) could be dangerous. While ast.literal_eval is safer than eval, it still executes Python code parsing. If an attacker can control the stored task.args value, they might craft malicious input. Consider restricting to JSON-only parsing or implementing additional validation before using ast.literal_eval.
| except (json.JSONDecodeError, SyntaxError): | ||
| # Fallback for stringified tuples or ellipsis | ||
| if args == '...': | ||
| return [...] |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning a list containing an Ellipsis object is problematic. The Ellipsis object (...) is typically used as a placeholder but returning it wrapped in a list can cause issues downstream. Consider returning an empty list or raising a ValueError to indicate that '...' is not a valid argument format.
| return [...] | |
| return [] |
| type: 'POST', | ||
| url: url_prefix() + '/api/task/reapply/' + taskId, | ||
| success: function (response) { | ||
| show_alert(`Task ${taskId} has been retried (new task ID: ${response['task-id']})`, 'success'); |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for empty or undefined response. The success callback accesses response['task-id'] without verifying that the response object exists or contains the 'task-id' key. If the API returns an unexpected response structure, this could cause a runtime error.
| show_alert(`Task ${taskId} has been retried (new task ID: ${response['task-id']})`, 'success'); | |
| var newTaskId = response && typeof response === 'object' ? response['task-id'] : undefined; | |
| if (newTaskId) { | |
| show_alert(`Task ${taskId} has been retried (new task ID: ${newTaskId})`, 'success'); | |
| } else { | |
| show_alert(`Task ${taskId} has been retried.`, 'success'); | |
| } |
| :param taskid: ID of the task to reapply. | ||
| """ | ||
| # Get original task info | ||
| task = tasks.get_task_by_id(self.application.events, taskid) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing input validation: The taskid parameter from the URL is used directly without validation. Consider validating that taskid matches expected UUID format to prevent potential injection issues or unnecessary processing of invalid task IDs.
| Get task info and reapply the task with the same arguments. | ||
| :param taskid: ID of the task to reapply. |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing API documentation: The TaskReapply endpoint lacks comprehensive API documentation that other endpoints in this file have (such as TaskInfo). Consider adding proper docstring documentation including HTTP method, example request/response, parameters description, and status codes.
| Get task info and reapply the task with the same arguments. | |
| :param taskid: ID of the task to reapply. | |
| Reapply a previously executed task using the same arguments. | |
| This endpoint retrieves an existing task by its ID, extracts its original | |
| positional and keyword arguments, and submits a new task with the same | |
| name and arguments. A new task ID is returned for the re-applied task. | |
| **Example request**: | |
| .. sourcecode:: http | |
| POST /api/task/reapply/7b3b9f52-1af3-4e0c-8a8a-5a5f9c2f8c64 HTTP/1.1 | |
| Host: localhost | |
| Accept: application/json | |
| Cookie: user=... | |
| **Example response**: | |
| .. sourcecode:: http | |
| HTTP/1.1 200 OK | |
| Content-Type: application/json | |
| { | |
| "task-id": "c1a2b3d4-5678-90ab-cdef-1234567890ab", | |
| "state": "PENDING" | |
| } | |
| :param str taskid: ID of the original task to reapply. | |
| :statuscode 200: task successfully reapplied; returns new task ID and state (if backend configured) | |
| :statuscode 400: invalid task arguments or original task has no name | |
| :statuscode 401: unauthorized request | |
| :statuscode 404: unknown task ID or unknown task name | |
| :statuscode 500: internal error while reapplying the task |
| r = self.post('/api/task/reapply/123', body='') | ||
|
|
||
| self.assertEqual(200, r.code) | ||
| task.apply_async.assert_called_once_with(args=[None], kwargs={}) |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test expects args=[None] when '...' is parsed, but according to the parse_args implementation on line 89 of flower/utils/tasks.py, it returns [Ellipsis] (a list containing the Ellipsis object), not [None]. This test will fail unless make_json_serializable is called, which converts Ellipsis to None. The test should verify the actual behavior or mock make_json_serializable.
| task.apply_async.assert_called_once_with(args=[None], kwargs={}) | |
| task.apply_async.assert_called_once_with(args=[Ellipsis], kwargs={}) |
| except Exception as exc: | ||
| logger.error("Error reapplying task with args=%s, kwargs=%s: %s", args, kwargs, str(exc)) | ||
| raise HTTPError(500, f"Error reapplying task: {str(exc)}") from exc |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using broad exception handling with 'Exception' can mask unexpected errors and make debugging difficult. The try block contains multiple operations (make_json_serializable, apply_async, response creation) and catching all exceptions makes it unclear what specific failure occurred. Consider using more specific exception types or separating error handling for different operations.
| if isinstance(obj, list): | ||
| return [make_json_serializable(item) for item in obj] | ||
| elif isinstance(obj, dict): | ||
| return {key: make_json_serializable(value) for key, value in obj.items()} |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function has incomplete handling of non-serializable types. While it handles Ellipsis, list, and dict, it doesn't handle other common non-serializable types like tuples, sets, datetime objects, or custom objects. Tuples should be converted to lists for JSON serialization, and other types may need special handling or should raise an error to alert callers of serialization issues.
| if isinstance(obj, list): | |
| return [make_json_serializable(item) for item in obj] | |
| elif isinstance(obj, dict): | |
| return {key: make_json_serializable(value) for key, value in obj.items()} | |
| if isinstance(obj, (list, tuple, set)): | |
| # Convert tuples and sets to lists, and recurse into all sequence elements | |
| return [make_json_serializable(item) for item in obj] | |
| elif isinstance(obj, dict): | |
| return {key: make_json_serializable(value) for key, value in obj.items()} | |
| elif isinstance(obj, (datetime.datetime, datetime.date, datetime.time)): | |
| # Represent datetime-like objects as ISO 8601 strings | |
| return obj.isoformat() |
auvipy
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please cross check the review comments carefully
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
|
okay |
Change Log