Enhancement: Add feature to manually retry failed task#1417
Enhancement: Add feature to manually retry failed task#1417yashpapa6969 wants to merge 8 commits intomher:masterfrom
Conversation
|
@mher anything I can do to help get this merged? |
sc68cal
left a comment
There was a problem hiding this comment.
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 |
|
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.
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.
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) |
There was a problem hiding this comment.
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 [...] |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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={}) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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()} |
There was a problem hiding this comment.
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.
please cross check the review comments carefully
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
okay |
Change Log