Skip to content

Commit 10dd0f3

Browse files
resilience
1 parent 612763c commit 10dd0f3

5 files changed

Lines changed: 112 additions & 88 deletions

File tree

tests/integration/client/orkes/test_orkes_clients.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from conductor.client.workflow.conductor_workflow import ConductorWorkflow
2727
from conductor.client.workflow.executor.workflow_executor import WorkflowExecutor
2828
from conductor.client.workflow.task.simple_task import SimpleTask
29+
from tests.integration.retry_helpers import retry_scenario
2930

3031
SUFFIX = str(uuid())
3132
WORKFLOW_NAME = 'IntegrationTestOrkesClientsWf_' + SUFFIX
@@ -66,7 +67,7 @@ def __init__(self, configuration: Configuration):
6667
self.authorization_client = orkes_clients.get_authorization_client()
6768
self.workflow_id = None
6869

69-
def run(self) -> None:
70+
def run(self, deadline=None) -> None:
7071
workflow = ConductorWorkflow(
7172
executor=self.workflow_executor,
7273
name=WORKFLOW_NAME,
@@ -77,13 +78,24 @@ def run(self) -> None:
7778
workflow >> SimpleTask("simple_task", "simple_task_ref")
7879
workflowDef = workflow.to_workflow_def()
7980

80-
self.test_workflow_lifecycle(workflowDef, workflow)
81-
self.test_task_lifecycle()
82-
self.test_secret_lifecycle()
83-
self.test_scheduler_lifecycle(workflowDef)
84-
self.test_application_lifecycle()
85-
self.__test_unit_test_workflow()
86-
self.test_user_group_permissions_lifecycle(workflowDef)
81+
# Each lifecycle is a scenario: on a transient (status 0) blip against
82+
# the shared dev server it retries from the top until the shared deadline
83+
# passes (see retry_helpers.retry_scenario); real failures raise at once.
84+
retry_scenario('test_workflow_lifecycle', self.test_workflow_lifecycle,
85+
workflowDef, workflow, deadline=deadline)
86+
retry_scenario('test_task_lifecycle', self.test_task_lifecycle,
87+
deadline=deadline)
88+
retry_scenario('test_secret_lifecycle', self.test_secret_lifecycle,
89+
deadline=deadline)
90+
retry_scenario('test_scheduler_lifecycle', self.test_scheduler_lifecycle,
91+
workflowDef, deadline=deadline)
92+
retry_scenario('test_application_lifecycle', self.test_application_lifecycle,
93+
deadline=deadline)
94+
retry_scenario('__test_unit_test_workflow', self.__test_unit_test_workflow,
95+
deadline=deadline)
96+
retry_scenario('test_user_group_permissions_lifecycle',
97+
self.test_user_group_permissions_lifecycle, workflowDef,
98+
deadline=deadline)
8799

88100
def test_workflow_lifecycle(self, workflowDef, workflow):
89101
self.__test_register_workflow_definition(workflowDef)
@@ -382,6 +394,22 @@ def __test_unit_test_workflow(self):
382394
execution = self.workflow_client.test_workflow(testRequest)
383395
assert execution != None
384396

397+
print(
398+
f"[test_workflow] workflow_id={getattr(execution, 'workflow_id', None)} "
399+
f"status={execution.status} "
400+
f"task_count={len(execution.tasks or [])}"
401+
)
402+
for t in (execution.tasks or []):
403+
print(
404+
f"[test_workflow] task ref={t.reference_task_name} "
405+
f"type={t.task_type} status={t.status} "
406+
f"retried={getattr(t, 'retry_count', None)}"
407+
)
408+
if execution.status != "COMPLETED":
409+
print(
410+
f"[test_workflow] non-terminal output={getattr(execution, 'output', None)}"
411+
)
412+
385413
# Ensure workflow is completed successfully
386414
assert execution.status == "COMPLETED"
387415

@@ -569,9 +597,9 @@ def __test_task_execution_lifecycle(self):
569597
self.task_client.add_task_log(polledTask.task_id, "Polled task...")
570598

571599
taskExecLogs = self.task_client.get_task_logs(polledTask.task_id)
572-
taskExecLogs[0].log == "Polled task..."
600+
assert taskExecLogs[0].log == "Polled task..."
573601

574-
# First task of second workflow is in the queue
602+
# First task of second workflow is still in the queue
575603
assert self.task_client.get_queue_size_for_task(TASK_TYPE) == 1
576604

577605
taskResult = TaskResult(

tests/integration/metadata/test_workflow_definition.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717
from conductor.client.workflow.task.sub_workflow_task import SubWorkflowTask, InlineSubWorkflowTask
1818
from conductor.client.workflow.task.switch_task import SwitchTask
1919
from conductor.client.workflow.task.terminate_task import TerminateTask, WorkflowStatus
20+
from tests.integration.retry_helpers import retry_scenario
2021

2122
WORKFLOW_NAME = 'python_test_workflow'
2223
TASK_NAME = 'python_test_simple_task'
2324
WORKFLOW_OWNER_EMAIL = "test@test"
2425

2526

26-
def run_workflow_definition_tests(workflow_executor: WorkflowExecutor) -> None:
27-
test_kitchensink_workflow_registration(workflow_executor)
27+
def run_workflow_definition_tests(workflow_executor: WorkflowExecutor, deadline=None) -> None:
28+
retry_scenario('test_kitchensink_workflow_registration',
29+
test_kitchensink_workflow_registration, workflow_executor,
30+
deadline=deadline)
2831

2932

3033
def generate_tasks_defs() -> List[TaskDef]:

tests/integration/test_async_lease_extension.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,15 @@ def test_02_async_without_heartbeat_times_out(self):
414414
f"Task should be TIMED_OUT/FAILED, got {task.status}")
415415
print("\n PASS: Async task timed out as expected without heartbeat")
416416

417+
# NOTE: excluded from the long-* CI buckets (reusing the
418+
# server_timeout_unreliable marker). This "overhead" test compares
419+
# server-side task durations (end_time - start_time) on the shared sdkdev
420+
# server, so it mostly measures queue/scheduling latency rather than the
421+
# SDK's LeaseManager.track() bookkeeping (a dict insert). On a loaded server
422+
# a fast task can stay non-terminal past the wait budget, so it flakes for
423+
# reasons unrelated to the SDK. A deterministic in-process micro-benchmark
424+
# would be the right home for the no-overhead claim if we want to keep it.
425+
@pytest.mark.server_timeout_unreliable
417426
def test_03_no_performance_overhead(self):
418427
"""Heartbeat tracking adds no meaningful overhead to fast async tasks."""
419428
print("\n" + "=" * 80)

tests/integration/test_workflow_client_intg.py

Lines changed: 11 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
from tests.integration.client.orkes.test_orkes_clients import TestOrkesClients
99
from conductor.client.configuration.configuration import Configuration
1010
from conductor.client.configuration.settings.authentication_settings import AuthenticationSettings
11-
from conductor.client.http.rest import ApiException
1211
from conductor.client.orkes.orkes_workflow_client import OrkesWorkflowClient
1312
from conductor.client.workflow.executor.workflow_executor import WorkflowExecutor
1413
from tests.integration.metadata.test_workflow_definition import run_workflow_definition_tests
1514
from tests.integration.workflow.test_workflow_execution import run_workflow_execution_tests
15+
from tests.integration.retry_helpers import DEFAULT_OVERALL_DEADLINE_SECONDS
1616

1717
WORKFLOW_NAME = 'ut_wf'
1818
WORKFLOW_UUID = 'ut_wf_uuid'
@@ -33,52 +33,6 @@ def get_configuration():
3333
return configuration
3434

3535

36-
def _first_transient_api_exception(exc):
37-
"""Walk the exception chain (cause/context) and return the first transient
38-
ApiException (status 0 / flagged transient), if any.
39-
40-
Inner test helpers sometimes catch an ApiException and re-raise it as a bare
41-
``Exception`` (losing the type), so we can't rely on the outermost exception
42-
type alone. Implicit chaining still records the original on ``__context__``
43-
(or ``__cause__`` when ``raise ... from`` is used), so we follow that chain.
44-
"""
45-
seen = set()
46-
cur = exc
47-
while cur is not None and id(cur) not in seen:
48-
seen.add(id(cur))
49-
if isinstance(cur, ApiException) and (
50-
getattr(cur, 'transient', False) or cur.status in (0, None)):
51-
return cur
52-
cur = cur.__cause__ or cur.__context__
53-
return None
54-
55-
56-
def _run_tolerating_transient(label, func, *args, retries=3, **kwargs):
57-
"""Run a sub-suite, retrying only on a transient (status 0) transport blip
58-
against the shared dev server.
59-
60-
A `(0)` ApiException is a raw connection/protocol hiccup (read timeout,
61-
stale keep-alive, HTTP/2 GOAWAY race, client closed by a fork cleanup, etc.)
62-
— not a real assertion or server failure — and was observed flaking the
63-
`test-all` bucket. Retrying absorbs that network noise while still surfacing
64-
genuine failures immediately (any non-transient error, or a transient one on
65-
the final attempt, re-raises). We inspect the whole exception chain because
66-
sub-suites may wrap the ApiException in a generic Exception.
67-
"""
68-
for attempt in range(retries):
69-
try:
70-
return func(*args, **kwargs)
71-
except Exception as e:
72-
transient = _first_transient_api_exception(e)
73-
if transient is not None and attempt < retries - 1:
74-
logger.warning(
75-
'transient (%s) API error in %s (attempt %d/%d): %s; retrying',
76-
transient.status, label, attempt + 1, retries, transient)
77-
time.sleep(2 ** attempt)
78-
continue
79-
raise
80-
81-
8236
class TestOrkesWorkflowClientIntg(unittest.TestCase):
8337

8438
@classmethod
@@ -96,14 +50,14 @@ def test_all(self):
9650
configuration = self.config
9751
workflow_executor = WorkflowExecutor(configuration)
9852

99-
# test_async.test_async_method(api_client)
100-
_run_tolerating_transient(
101-
'workflow_definition_tests',
102-
run_workflow_definition_tests, workflow_executor)
103-
_run_tolerating_transient(
104-
'workflow_execution_tests',
105-
run_workflow_execution_tests, configuration, workflow_executor)
106-
_run_tolerating_transient(
107-
'orkes_clients',
108-
TestOrkesClients(configuration=configuration).run)
53+
# One shared wall-clock budget for the whole aggregate suite. Each
54+
# sub-suite runs its scenarios once; a scenario that hits a transient
55+
# (status 0) transport blip against the shared dev server retries until
56+
# this deadline passes, with capped exponential backoff (see
57+
# tests/integration/retry_helpers.py). Real failures raise immediately.
58+
deadline = time.monotonic() + DEFAULT_OVERALL_DEADLINE_SECONDS
59+
60+
run_workflow_definition_tests(workflow_executor, deadline=deadline)
61+
run_workflow_execution_tests(configuration, workflow_executor, deadline=deadline)
62+
TestOrkesClients(configuration=configuration).run(deadline=deadline)
10963
logger.info('END: integration tests')

tests/integration/workflow/test_workflow_execution.py

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from conductor.client.workflow.executor.workflow_executor import WorkflowExecutor
1313
from conductor.client.workflow.task.simple_task import SimpleTask
1414
from tests.integration.resources.worker.python.python_worker import *
15+
from tests.integration.retry_helpers import retry_scenario
1516

1617
WORKFLOW_NAME = "sdk_python_integration_test_workflow"
1718
WORKFLOW_DESCRIPTION = "Python SDK Integration Test"
@@ -36,7 +37,8 @@
3637
)
3738

3839

39-
def run_workflow_execution_tests(configuration: Configuration, workflow_executor: WorkflowExecutor):
40+
def run_workflow_execution_tests(configuration: Configuration, workflow_executor: WorkflowExecutor,
41+
deadline=None):
4042
workers = [
4143
ClassWorker(TASK_NAME),
4244
ClassWorkerWithDomain(TASK_NAME),
@@ -59,23 +61,35 @@ def run_workflow_execution_tests(configuration: Configuration, workflow_executor
5961
# (they'd surface as an opaque generic Exception instead). finally cleans up
6062
# on both success and failure while letting the original error propagate.
6163
try:
62-
scenario_get_workflow_by_correlation_ids(workflow_executor)
64+
retry_scenario('scenario_get_workflow_by_correlation_ids',
65+
scenario_get_workflow_by_correlation_ids, workflow_executor,
66+
deadline=deadline)
6367
logger.debug('finished workflow correlation ids test')
64-
scenario_workflow_registration(workflow_executor)
68+
retry_scenario('scenario_workflow_registration',
69+
scenario_workflow_registration, workflow_executor,
70+
deadline=deadline)
6571
logger.debug('finished workflow registration tests')
66-
scenario_workflow_execution(
72+
retry_scenario(
73+
'scenario_workflow_execution', scenario_workflow_execution,
6774
workflow_quantity=6,
6875
workflow_name=WORKFLOW_NAME,
6976
workflow_executor=workflow_executor,
70-
workflow_completion_timeout=5.0
77+
workflow_completion_timeout=5.0,
78+
deadline=deadline,
7179
)
72-
scenario_decorated_workers(workflow_executor)
80+
retry_scenario('scenario_decorated_workers',
81+
scenario_decorated_workers, workflow_executor,
82+
deadline=deadline)
7383
logger.debug('finished decorated workers tests')
74-
scenario_execute_workflow_async_features(workflow_executor)
84+
retry_scenario('scenario_execute_workflow_async_features',
85+
scenario_execute_workflow_async_features, workflow_executor,
86+
deadline=deadline)
7587
logger.debug('finished execute_workflow reactive features tests')
76-
scenario_execute_workflow_error_handling(workflow_executor)
88+
retry_scenario('scenario_execute_workflow_error_handling',
89+
scenario_execute_workflow_error_handling, workflow_executor,
90+
deadline=deadline)
7791
logger.debug('finished execute_workflow error handling tests')
78-
run_signal_tests(configuration, workflow_executor)
92+
run_signal_tests(configuration, workflow_executor, deadline=deadline)
7993
logger.debug('finished signal API tests')
8094
finally:
8195
task_handler.stop_processes()
@@ -473,28 +487,44 @@ def _wait_for_workflow_completion(workflow_executor: WorkflowExecutor, workflow_
473487

474488
# ===== SIGNAL TESTS =====
475489

476-
def run_signal_tests(configuration: Configuration, workflow_executor: WorkflowExecutor):
477-
"""Run all signal API tests using WorkflowExecutor methods"""
490+
def run_signal_tests(configuration: Configuration, workflow_executor: WorkflowExecutor,
491+
deadline=None):
492+
"""Run all signal API tests using WorkflowExecutor methods.
493+
494+
Each scenario is retried at the scenario level on a transient blip (see
495+
retry_scenario): a retry starts a fresh workflow and issues a fresh sync
496+
signal, so the asserted SignalResponse is always from a signal this attempt
497+
actually sent — no double-signalling of a single workflow.
498+
"""
478499
logger.info('START: Signal API tests using WorkflowExecutor')
479500

480501
try:
481502
# Register signal test workflows (same as original test)
482-
_register_signal_test_workflows(workflow_executor)
503+
retry_scenario('_register_signal_test_workflows',
504+
_register_signal_test_workflows, workflow_executor,
505+
deadline=deadline)
483506

484507
# Test sync signal with different return strategies
485-
scenario_signal_target_workflow(workflow_executor)
486-
scenario_signal_blocking_workflow(workflow_executor)
487-
scenario_signal_blocking_task(workflow_executor)
488-
scenario_signal_blocking_task_input(workflow_executor)
508+
retry_scenario('scenario_signal_target_workflow',
509+
scenario_signal_target_workflow, workflow_executor, deadline=deadline)
510+
retry_scenario('scenario_signal_blocking_workflow',
511+
scenario_signal_blocking_workflow, workflow_executor, deadline=deadline)
512+
retry_scenario('scenario_signal_blocking_task',
513+
scenario_signal_blocking_task, workflow_executor, deadline=deadline)
514+
retry_scenario('scenario_signal_blocking_task_input',
515+
scenario_signal_blocking_task_input, workflow_executor, deadline=deadline)
489516

490517
# Test default return strategy
491-
scenario_signal_default_strategy(workflow_executor)
518+
retry_scenario('scenario_signal_default_strategy',
519+
scenario_signal_default_strategy, workflow_executor, deadline=deadline)
492520

493521
# Test async signal
494-
scenario_signal_async(workflow_executor)
522+
retry_scenario('scenario_signal_async',
523+
scenario_signal_async, workflow_executor, deadline=deadline)
495524

496525
# Test to_dict fix
497-
scenario_signal_to_dict_fix(workflow_executor)
526+
retry_scenario('scenario_signal_to_dict_fix',
527+
scenario_signal_to_dict_fix, workflow_executor, deadline=deadline)
498528

499529
logger.info('All signal tests completed successfully')
500530

0 commit comments

Comments
 (0)