|
23 | 23 | from conductor.client.http.models.task import Task |
24 | 24 | from conductor.client.http.models.task_result import TaskResult |
25 | 25 | from conductor.client.http.models.task_result_status import TaskResultStatus |
26 | | -from conductor.client.http.rest import AuthorizationException |
| 26 | +from conductor.client.http.rest import AuthorizationException, ApiException |
27 | 27 | from conductor.client.worker.worker_interface import WorkerInterface |
28 | 28 |
|
29 | 29 |
|
@@ -803,6 +803,150 @@ def test_update_task_with_metrics_on_error(self): |
803 | 803 | 4 |
804 | 804 | ) |
805 | 805 |
|
| 806 | + # ======================================== |
| 807 | + # v1 Fallback Tests (backward compat with Orkes Conductor < v5) |
| 808 | + # ======================================== |
| 809 | + |
| 810 | + @patch('time.sleep', Mock(return_value=None)) |
| 811 | + def test_update_task_v2_404_falls_back_to_v1(self): |
| 812 | + """When server returns 404 for v2 endpoint, should fall back to v1 and return None.""" |
| 813 | + worker = MockWorker('test_task') |
| 814 | + task_runner = TaskRunner(worker=worker) |
| 815 | + |
| 816 | + task_result = TaskResult( |
| 817 | + task_id='test_id', |
| 818 | + workflow_instance_id='wf_id', |
| 819 | + worker_id=worker.get_identity(), |
| 820 | + status=TaskResultStatus.COMPLETED |
| 821 | + ) |
| 822 | + |
| 823 | + with patch.object(TaskResourceApi, 'update_task_v2', |
| 824 | + side_effect=ApiException(status=404)) as mock_v2, \ |
| 825 | + patch.object(TaskResourceApi, 'update_task', return_value='ok') as mock_v1: |
| 826 | + result = task_runner._TaskRunner__update_task(task_result) |
| 827 | + |
| 828 | + mock_v2.assert_called_once() |
| 829 | + mock_v1.assert_called_once() |
| 830 | + self.assertIsNone(result) |
| 831 | + |
| 832 | + @patch('time.sleep', Mock(return_value=None)) |
| 833 | + def test_update_task_v2_404_sets_v1_flag(self): |
| 834 | + """After a 404 on v2, _use_update_v2 flag must be False.""" |
| 835 | + worker = MockWorker('test_task') |
| 836 | + task_runner = TaskRunner(worker=worker) |
| 837 | + self.assertTrue(task_runner._use_update_v2) |
| 838 | + |
| 839 | + task_result = TaskResult( |
| 840 | + task_id='test_id', |
| 841 | + workflow_instance_id='wf_id', |
| 842 | + worker_id=worker.get_identity(), |
| 843 | + status=TaskResultStatus.COMPLETED |
| 844 | + ) |
| 845 | + |
| 846 | + with patch.object(TaskResourceApi, 'update_task_v2', |
| 847 | + side_effect=ApiException(status=404)), \ |
| 848 | + patch.object(TaskResourceApi, 'update_task', return_value='ok'): |
| 849 | + task_runner._TaskRunner__update_task(task_result) |
| 850 | + |
| 851 | + self.assertFalse(task_runner._use_update_v2) |
| 852 | + |
| 853 | + @patch('time.sleep', Mock(return_value=None)) |
| 854 | + def test_update_task_uses_v1_only_after_flag_set(self): |
| 855 | + """Once _use_update_v2 is False, v2 is never called again.""" |
| 856 | + worker = MockWorker('test_task') |
| 857 | + task_runner = TaskRunner(worker=worker) |
| 858 | + task_runner._use_update_v2 = False # pre-set as if fallback already happened |
| 859 | + |
| 860 | + task_result = TaskResult( |
| 861 | + task_id='test_id', |
| 862 | + workflow_instance_id='wf_id', |
| 863 | + worker_id=worker.get_identity(), |
| 864 | + status=TaskResultStatus.COMPLETED |
| 865 | + ) |
| 866 | + |
| 867 | + with patch.object(TaskResourceApi, 'update_task_v2') as mock_v2, \ |
| 868 | + patch.object(TaskResourceApi, 'update_task', return_value='ok') as mock_v1: |
| 869 | + result = task_runner._TaskRunner__update_task(task_result) |
| 870 | + |
| 871 | + mock_v2.assert_not_called() |
| 872 | + mock_v1.assert_called_once() |
| 873 | + self.assertIsNone(result) |
| 874 | + |
| 875 | + @patch('time.sleep', Mock(return_value=None)) |
| 876 | + def test_update_task_non_404_api_exception_does_not_fallback(self): |
| 877 | + """A non-404 ApiException (e.g. 500) should not trigger v1 fallback.""" |
| 878 | + worker = MockWorker('test_task') |
| 879 | + task_runner = TaskRunner(worker=worker) |
| 880 | + |
| 881 | + task_result = TaskResult( |
| 882 | + task_id='test_id', |
| 883 | + workflow_instance_id='wf_id', |
| 884 | + worker_id=worker.get_identity(), |
| 885 | + status=TaskResultStatus.COMPLETED |
| 886 | + ) |
| 887 | + |
| 888 | + with patch.object(TaskResourceApi, 'update_task_v2', |
| 889 | + side_effect=ApiException(status=500)) as mock_v2, \ |
| 890 | + patch.object(TaskResourceApi, 'update_task') as mock_v1: |
| 891 | + result = task_runner._TaskRunner__update_task(task_result) |
| 892 | + |
| 893 | + # v2 called 4 times (all retries), v1 never called, flag unchanged |
| 894 | + self.assertEqual(mock_v2.call_count, 4) |
| 895 | + mock_v1.assert_not_called() |
| 896 | + self.assertTrue(task_runner._use_update_v2) |
| 897 | + self.assertIsNone(result) |
| 898 | + |
| 899 | + @patch('time.sleep', Mock(return_value=None)) |
| 900 | + def test_execute_and_update_task_tight_loop_with_v1_polls_for_next(self): |
| 901 | + """When v1 is used, the tight loop should poll immediately for the next task.""" |
| 902 | + worker = MockWorker('test_task') |
| 903 | + task_runner = TaskRunner(worker=worker) |
| 904 | + task_runner._use_update_v2 = False # simulate post-fallback state |
| 905 | + |
| 906 | + first_task = Task(task_id='task_1', workflow_instance_id='wf_1') |
| 907 | + second_task = Task(task_id='task_2', workflow_instance_id='wf_1') |
| 908 | + |
| 909 | + # Execute returns a result, update v1 returns None, poll returns second task then empty |
| 910 | + with patch.object(TaskResourceApi, 'update_task', return_value='ok') as mock_v1, \ |
| 911 | + patch.object(TaskResourceApi, 'batch_poll', |
| 912 | + side_effect=[[second_task], []]) as mock_poll: |
| 913 | + task_runner._TaskRunner__execute_and_update_task(first_task) |
| 914 | + |
| 915 | + # update_task called twice (once per task), poll called twice (second_task then empty) |
| 916 | + self.assertEqual(mock_v1.call_count, 2) |
| 917 | + self.assertEqual(mock_poll.call_count, 2) |
| 918 | + |
| 919 | + @patch('time.sleep', Mock(return_value=None)) |
| 920 | + def test_execute_and_update_task_tight_loop_stops_when_queue_empty_on_v1(self): |
| 921 | + """With v1, if poll returns nothing the tight loop exits cleanly.""" |
| 922 | + worker = MockWorker('test_task') |
| 923 | + task_runner = TaskRunner(worker=worker) |
| 924 | + task_runner._use_update_v2 = False |
| 925 | + |
| 926 | + task = Task(task_id='task_1', workflow_instance_id='wf_1') |
| 927 | + |
| 928 | + with patch.object(TaskResourceApi, 'update_task', return_value='ok') as mock_v1, \ |
| 929 | + patch.object(TaskResourceApi, 'batch_poll', return_value=[]) as mock_poll: |
| 930 | + task_runner._TaskRunner__execute_and_update_task(task) |
| 931 | + |
| 932 | + mock_v1.assert_called_once() |
| 933 | + mock_poll.assert_called_once() |
| 934 | + |
| 935 | + @patch('time.sleep', Mock(return_value=None)) |
| 936 | + def test_execute_and_update_task_tight_loop_not_pollled_when_v2(self): |
| 937 | + """With v2, poll is NOT called inside the tight loop (v2 returns next task directly).""" |
| 938 | + worker = MockWorker('test_task') |
| 939 | + task_runner = TaskRunner(worker=worker) |
| 940 | + |
| 941 | + first_task = Task(task_id='task_1', workflow_instance_id='wf_1') |
| 942 | + |
| 943 | + with patch.object(TaskResourceApi, 'update_task_v2', return_value=None) as mock_v2, \ |
| 944 | + patch.object(TaskResourceApi, 'batch_poll') as mock_poll: |
| 945 | + task_runner._TaskRunner__execute_and_update_task(first_task) |
| 946 | + |
| 947 | + mock_v2.assert_called_once() |
| 948 | + mock_poll.assert_not_called() |
| 949 | + |
806 | 950 | # ======================================== |
807 | 951 | # Property and Environment Tests |
808 | 952 | # ======================================== |
|
0 commit comments