1111import yaml
1212
1313from tangle_cli import cli , pipeline_run_manager , pipeline_runs_cli
14- from tangle_cli .pipeline_runner import PipelineRunner , PipelineRunnerHooks
1514from tangle_cli .pipeline_run_manager import (
15+ AmbiguousPipelineRunRecoveryError ,
1616 PipelineRunContext ,
17+ PipelineRunError ,
1718 PipelineRunHooks ,
1819 PipelineRunManager ,
19- PipelineRunError ,
2020 PipelineWaitOutcome ,
2121 PipelineWaitPoll ,
2222)
23+ from tangle_cli .pipeline_runner import PipelineRunner , PipelineRunnerHooks
2324
2425
2526def run_app (app , args : list [str ]) -> None :
@@ -199,7 +200,10 @@ def test_pipeline_runs_submit_dry_run_prints_sanitized_payload(monkeypatch, tmp_
199200 "arguments" : {"config" : {"_meta" : {"mode" : "keep" }}},
200201 "componentRef" : {
201202 "name" : "text-component" ,
202- "text" : "name: Text Component\n _source_dir: /tmp/private\n implementation:\n container:\n image: busybox\n " ,
203+ "text" : (
204+ "name: Text Component\n _source_dir: /tmp/private\n implementation:\n "
205+ " container:\n image: busybox\n "
206+ ),
203207 }
204208 }
205209 }
@@ -970,7 +974,9 @@ def fake_regenerate_yaml(self, **kwargs):
970974
971975 assert json .loads (capsys .readouterr ().out )["id" ] == "run-1"
972976 assert regenerated == [outside_python .resolve ()]
973- submitted_task = fake_client .created [0 ]["root_task" ]["componentRef" ]["spec" ]["implementation" ]["graph" ]["tasks" ]["generated" ]
977+ submitted_task = fake_client .created [0 ]["root_task" ]["componentRef" ]["spec" ]["implementation" ]["graph" ][
978+ "tasks"
979+ ]["generated" ]
974980 assert submitted_task ["componentRef" ]["name" ] == "Submit Generated Component"
975981
976982
@@ -1670,9 +1676,9 @@ def after_poll(self, poll, context):
16701676 ]
16711677
16721678
1673-
16741679def test_pipeline_runs_submit_failure_recovery_adopts_existing_run (tmp_path : Path , monkeypatch ) -> None :
1675- monkeypatch .setattr ("tangle_cli.pipeline_run_manager.time.sleep" , lambda _seconds : None )
1680+ sleeps : list [float ] = []
1681+ monkeypatch .setattr ("tangle_cli.pipeline_run_manager.time.sleep" , lambda value : sleeps .append (value ))
16761682 pipeline_path = tmp_path / "pipeline.yaml"
16771683
16781684 class DynamicTimeHooks (PipelineRunnerHooks ):
@@ -1727,18 +1733,82 @@ def pipeline_runs_list(self, **kwargs: Any) -> dict[str, Any]:
17271733 assert hooks .prepare_run_arguments_calls == 1
17281734 assert hooks .submit_errors == []
17291735 assert len (client .created ) == 1
1736+ assert sleeps == [0.5 ]
17301737 submission_id = client .created [0 ]["annotations" ]["tangle-cli/submission-id" ]
17311738 assert submission_id
17321739 assert len (client .list_calls ) == 1
17331740 assert "tangle-cli/submission-id" in client .list_calls [0 ]["filter_query" ]
17341741 assert submission_id in client .list_calls [0 ]["filter_query" ]
17351742
17361743
1744+ def test_pipeline_runs_submit_failure_recovery_waits_for_delayed_registration (monkeypatch ) -> None :
1745+ sleeps : list [float ] = []
1746+ monkeypatch .setattr ("tangle_cli.pipeline_run_manager.time.sleep" , lambda value : sleeps .append (value ))
1747+
1748+ class DelayedRecoveryClient (FakeClient ):
1749+ def pipeline_runs_create (self , body : Any = None ) -> dict [str , Any ]:
1750+ self .created .append (copy .deepcopy (body ))
1751+ raise TimeoutError ("submit timed out" )
1752+
1753+ def pipeline_runs_list (self , ** kwargs : Any ) -> dict [str , Any ]:
1754+ self .list_calls .append (kwargs )
1755+ if len (self .list_calls ) <= 2 :
1756+ return {"pipeline_runs" : [], "next_page_token" : None }
1757+ return {
1758+ "pipeline_runs" : [{"id" : "run-created" , "root_execution_id" : "exec-created" }],
1759+ "next_page_token" : None ,
1760+ }
1761+
1762+ client = DelayedRecoveryClient ()
1763+ manager = PipelineRunManager (client = client )
1764+ body = {"root_task" : {"componentRef" : {"spec" : {"name" : "delayed-recovery" }}}}
1765+
1766+ result = manager .run_prepared_body (body )
1767+
1768+ assert result ["response" ]["id" ] == "run-created"
1769+ assert result ["context" ].metadata ["recovered_after_submit_error" ] is True
1770+ assert len (client .created ) == 1
1771+ assert len (client .list_calls ) == 3
1772+ assert sleeps == [0.5 , 1.0 , 2.0 ]
1773+
1774+
1775+ def test_pipeline_runs_submit_failure_recovery_refuses_ambiguous_matches (monkeypatch ) -> None :
1776+ sleeps : list [float ] = []
1777+ monkeypatch .setattr ("tangle_cli.pipeline_run_manager.time.sleep" , lambda value : sleeps .append (value ))
1778+
1779+ class AmbiguousRecoveryClient (FakeClient ):
1780+ def pipeline_runs_create (self , body : Any = None ) -> dict [str , Any ]:
1781+ self .created .append (copy .deepcopy (body ))
1782+ raise TimeoutError ("submit timed out" )
1783+
1784+ def pipeline_runs_list (self , ** kwargs : Any ) -> dict [str , Any ]:
1785+ self .list_calls .append (kwargs )
1786+ return {
1787+ "pipeline_runs" : [
1788+ {"id" : "run-created-1" , "root_execution_id" : "exec-created-1" },
1789+ {"id" : "run-created-2" , "root_execution_id" : "exec-created-2" },
1790+ ],
1791+ "next_page_token" : None ,
1792+ }
1793+
1794+ client = AmbiguousRecoveryClient ()
1795+ manager = PipelineRunManager (client = client )
1796+ body = {"root_task" : {"componentRef" : {"spec" : {"name" : "ambiguous-recovery" }}}}
1797+
1798+ with pytest .raises (AmbiguousPipelineRunRecoveryError ):
1799+ manager .run_prepared_body (body , max_attempts = 2 )
1800+
1801+ assert len (client .created ) == 1
1802+ assert len (client .list_calls ) == 1
1803+ assert sleeps == [0.5 ]
1804+
1805+
17371806def test_pipeline_runs_submit_failure_reuses_frozen_body_when_recovery_finds_no_run (
17381807 tmp_path : Path ,
17391808 monkeypatch ,
17401809) -> None :
1741- monkeypatch .setattr ("tangle_cli.pipeline_run_manager.time.sleep" , lambda _seconds : None )
1810+ sleeps : list [float ] = []
1811+ monkeypatch .setattr ("tangle_cli.pipeline_run_manager.time.sleep" , lambda value : sleeps .append (value ))
17421812 pipeline_path = tmp_path / "pipeline.yaml"
17431813
17441814 class DynamicTimeHooks (PipelineRunnerHooks ):
@@ -1799,12 +1869,14 @@ def pipeline_runs_list(self, **kwargs: Any) -> dict[str, Any]:
17991869 assert client .created [0 ]["annotations" ]["tangle-cli/submission-id" ] == client .created [1 ]["annotations" ][
18001870 "tangle-cli/submission-id"
18011871 ]
1802- assert len (client .list_calls ) == 4
1803-
1872+ assert len (client .list_calls ) == 2 * len (pipeline_run_manager ._SUBMIT_RECOVERY_BACKOFF_SECONDS )
1873+ expected_sleeps = list (pipeline_run_manager ._SUBMIT_RECOVERY_BACKOFF_SECONDS )
1874+ assert sleeps == expected_sleeps + expected_sleeps
18041875
18051876
18061877def test_pipeline_runs_recovered_retry_runs_after_retry_submit_hook (tmp_path : Path , monkeypatch ) -> None :
1807- monkeypatch .setattr ("tangle_cli.pipeline_run_manager.time.sleep" , lambda _seconds : None )
1878+ sleeps : list [float ] = []
1879+ monkeypatch .setattr ("tangle_cli.pipeline_run_manager.time.sleep" , lambda value : sleeps .append (value ))
18081880 pipeline_path = tmp_path / "pipeline.yaml"
18091881 events : list [tuple [str , int , str | None ]] = []
18101882
@@ -1842,7 +1914,7 @@ def pipeline_runs_create(self, body: Any = None) -> dict[str, Any]:
18421914
18431915 def pipeline_runs_list (self , ** kwargs : Any ) -> dict [str , Any ]:
18441916 self .list_calls .append (kwargs )
1845- if len (self .list_calls ) <= 2 :
1917+ if len (self .list_calls ) <= len ( pipeline_run_manager . _SUBMIT_RECOVERY_BACKOFF_SECONDS ) :
18461918 return {"pipeline_runs" : [], "next_page_token" : None }
18471919 return {
18481920 "pipeline_runs" : [
@@ -1862,6 +1934,8 @@ def pipeline_runs_list(self, **kwargs: Any) -> dict[str, Any]:
18621934 assert result ["context" ].metadata ["recovered_after_submit_error" ] is True
18631935 assert hooks .prepare_run_arguments_calls == 1
18641936 assert len (client .created ) == 1
1937+ assert len (client .list_calls ) == len (pipeline_run_manager ._SUBMIT_RECOVERY_BACKOFF_SECONDS ) + 1
1938+ assert sleeps == [0.5 , 1.0 , 2.0 , 0.5 ]
18651939 assert events == [("before_retry" , 2 , None ), ("after_retry_submit" , 2 , "run-created" )]
18661940
18671941
0 commit comments