Add endpoint to rollback failed imports#613
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new script, retry_failed_ingestion.py, along with its corresponding unit tests, to automate the rollback and retry process for failed Dataflow ingestion jobs. Feedback on the implementation highlights a critical correctness bug where the workflow could be retriggered even if a rollback fails, and a missing pagination implementation when fetching Dataflow job messages. Additionally, it is recommended to use posixpath instead of os.path for GCS path manipulations to avoid platform-specific path separator issues, and to clean up unused imports in the test file.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Documentation | 19 minor |
| CodeStyle | 9 minor |
| Complexity | 2 medium |
🟢 Metrics 74 complexity
Metric Results Complexity 74
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
a3696c4 to
5bb4e0a
Compare
| logging.error(f"Error parsing importList parameter: {e}") | ||
|
|
||
| logging.info(f"Fetching error messages for Dataflow job {job_id}...") | ||
| request = dataflow.projects().locations().jobs().messages().list( |
There was a problem hiding this comment.
This seems to only be checking dataflow for failures. What if the failures came from elsewhere in the worfklow (spanner embeddings, ingestion helper issue, bq federation).
What's the long term plan for this, should we be checking all the stages? Should the workflow be modified to propoagate errors from dataflow? (if that's even possible)
There was a problem hiding this comment.
Yes, we should be taking bq failures also into account. It will require us to track aggregation status into spanner. Looking into this.
There was a problem hiding this comment.
The new implementation is an endpoint within ingestion helper. It takes a workflow id as an argument and reverts all the imports for that workflow. It can be invoked manually or from the ingestion workflow in the failure handing logic for any type of failures.
| """ | ||
| status_results = transaction.execute_sql( | ||
| status_sql, | ||
| params={'importName': short_name}, |
There was a problem hiding this comment.
How would this work for a multi-provenance import?
Like if I import WHO and ILO at once?
There was a problem hiding this comment.
The script checks for the dataflow logs to see if it finds a specific failed import and reverts it. Else, it falls back to reverting all the imports in that batch.
There was a problem hiding this comment.
I am also thinking that it may be simpler to revert the whole batch as a v1 for the rollback flow.
There was a problem hiding this comment.
The new implementation reverts the whole batch
gmechali
left a comment
There was a problem hiding this comment.
hey Vishal, overall I think the major concern I have is that I think the ingesiton helper is being used for too much. It shouldn't contain such major implementation logic, I think all that should live within the workflow, and the ingestion helper should just give the workflow the information it needs (like the last successful graphpath).
Happy to talk this through if you want. lmk what you think
| logging.error(f"Error querying IngestionHistory for workflow '{workflow_id}': {e}") | ||
| return [] | ||
|
|
||
| def revert_import(self, |
There was a problem hiding this comment.
Can you move most of this logic in a separate file in util/rollback_helper.py or something?
And just keep the spanner code in here, so get_imports_for_workflow, fetch_import_version_history, set_rollback_status etc..
I think this spanner client should just be simple helper functions to execute spanner queries. The actual logic for how these helpers are called belong elsewhere.
| logging.error(f'Error checking for retry imports: {e}') | ||
| return True | ||
|
|
||
| def get_imports_for_workflow(self, workflow_id: str) -> list[str]: |
There was a problem hiding this comment.
ultra nit: "from" instead of for?
|
|
||
| if not rows_history: | ||
| logging.warning( | ||
| f"No version history found in ImportVersionHistory for '{short_name}'. Cannot revert.") |
There was a problem hiding this comment.
So what should happen here to the database?
do we leave the run in FAILED state, the staleness timestamp should read before this fialed run start. Ideally they get alerted of this failuyre and have to manually intervene, is that right?
| latest_version = rows_history[0][0] | ||
| latest_comment = rows_history[0][1] if len(rows_history[0]) > 1 else "" | ||
|
|
||
| if latest_comment and ("Reverted due to Dataflow failure" in latest_comment or "Reverted batch workflow" in latest_comment): |
There was a problem hiding this comment.
Using these string comments seems super dangerous to me. Shouldn't we use the Status instead?
It's really not transparent that if you modify the comment, you ll modify actual workflow behavior. You know this now, but whoever is working on this in 9 months won't. Can't we just fetch the Status on IngestionHistory row, add a new rollback status and use that to determine the state?
| logging.error( | ||
| f"Import '{short_name}' was already reverted recently. Aborting to prevent infinite loop." | ||
| ) | ||
| return "ALREADY_REVERTED", None |
There was a problem hiding this comment.
What exactly is this function supposed to return? I see you returning a True/False or now thing "ALREADY_REVERTED" string.
|
|
||
| class RollbackResponse(BaseResponse): | ||
| revertedImports: Dict[str, str] = Field(default_factory=dict, description="Map of import names to their reverted versions") | ||
| dryRun: bool = Field(default=False, description="Whether this was executed in dry run mode") |
There was a problem hiding this comment.
Why do we need the dry run in the response?
| ) | ||
| return "ALREADY_REVERTED", None | ||
|
|
||
| previous_version = None |
There was a problem hiding this comment.
I kind of disagree with the architecture of this. I think the rollback shouldn't be triggered by the ingestion helper. I think the ingestion helper should be called to give the worfklow the information it needs to do the rollback.
I think we can use a subworkflow to package everything from updating the ingesiton_history, to dataflow, to aggregations.
Then the worfklow, on failure, can aks the ingestion Helper for the graphPath of the last successful ingestions for these imports. Then the worfklow can re-update the ingestionHistory/Importversionhistory statuses, re-laucnh dataflow with new params, relaunch postprocessing etc...
Otherwise, with this implementation, we're just complete obfuscating the process for the rollback.
|
|
||
| logging.info(f"Retriggering workflow '{workflow_name}'...") | ||
| try: | ||
| service = build('workflowexecutions', 'v1', cache_discovery=False) |
There was a problem hiding this comment.
I dont think we should make this a separate workflow, but a subworkflow.
The first workflow acquires the lock, I believe it should NOT be allowed to release the lock until the database is back in a good state post rollback. That's why I think it should also be a subworkflow.
No description provided.