|
1 | | -from py_trees.composites import Selector, Sequence |
2 | | -from py_trees.decorators import Inverter, Repeat, Retry, Timeout |
| 1 | +from typing import Iterator, List, Optional |
| 2 | +from typing import Sequence as TypingSequence |
| 3 | + |
| 4 | +from py_trees.behaviour import Behaviour |
| 5 | +from py_trees.common import OneShotPolicy, Status |
| 6 | +from py_trees.composites import Composite, Selector, Sequence |
| 7 | +from py_trees.decorators import Repeat, Retry, SuccessIsRunning, Timeout |
3 | 8 |
|
4 | 9 | from ..github_client_async import GitHubClientAsync |
5 | 10 | from .behaviours import ( |
|
14 | 19 | IsWorkflowIdentified, |
15 | 20 | IsWorkflowSuccessful, |
16 | 21 | IsWorkflowTriggered, |
| 22 | + ResetPackageState, |
| 23 | + ResetWorkflowState, |
17 | 24 | Sleep, |
18 | 25 | ) |
19 | 26 | from .behaviours import TriggerWorkflow as TriggerWorkflow |
20 | 27 | from .behaviours import UpdateWorkflowStatus |
21 | 28 | from .decorators import FlagGuard |
22 | | -from .state import PackageMeta, ReleaseMeta, Workflow |
| 29 | +from .state import Package, PackageMeta, ReleaseMeta, Workflow |
| 30 | + |
| 31 | + |
| 32 | +class ParallelBarrier(Composite): |
| 33 | + """ |
| 34 | + A simplified parallel composite that runs all children until convergence. |
| 35 | +
|
| 36 | + This parallel composite: |
| 37 | + - Ticks all children on each tick |
| 38 | + - Skips children that have already converged (SUCCESS or FAILURE) in synchronized mode |
| 39 | + - Returns FAILURE if any child returns FAILURE |
| 40 | + - Returns SUCCESS if all children return SUCCESS |
| 41 | + - Returns RUNNING if any child is still RUNNING |
| 42 | +
|
| 43 | + Unlike py_trees.Parallel, this composite: |
| 44 | + - Has no policy configuration (always waits for all children) |
| 45 | + - Always operates in synchronized mode (skips converged children) |
| 46 | + - Has simpler logic focused on the all-must-succeed pattern |
| 47 | +
|
| 48 | + Args: |
| 49 | + name: the composite behaviour name |
| 50 | + children: list of children to add |
| 51 | + """ |
| 52 | + |
| 53 | + def __init__( |
| 54 | + self, |
| 55 | + name: str, |
| 56 | + children: Optional[TypingSequence[Behaviour]] = None, |
| 57 | + ): |
| 58 | + super().__init__(name, children) |
| 59 | + |
| 60 | + def tick(self) -> Iterator[Behaviour]: |
| 61 | + """ |
| 62 | + Tick all children until they converge, then determine status. |
| 63 | + """ |
| 64 | + # Initialise if first time |
| 65 | + if self.status != Status.RUNNING: |
| 66 | + # subclass (user) handling |
| 67 | + self.initialise() |
| 68 | + |
| 69 | + # Handle empty children case |
| 70 | + if not self.children: |
| 71 | + self.current_child = None |
| 72 | + self.stop(Status.SUCCESS) |
| 73 | + yield self |
| 74 | + return |
| 75 | + |
| 76 | + # Tick all children, skipping those that have already converged |
| 77 | + for child in self.children: |
| 78 | + # Skip children that have already converged (synchronized mode) |
| 79 | + if child.status in [Status.SUCCESS, Status.FAILURE]: |
| 80 | + continue |
| 81 | + # Tick the child |
| 82 | + for node in child.tick(): |
| 83 | + yield node |
| 84 | + |
| 85 | + # Determine new status based on children's statuses |
| 86 | + self.current_child = self.children[-1] |
| 87 | + |
| 88 | + new_status = Status.INVALID |
| 89 | + has_running = any(child.status == Status.RUNNING for child in self.children) |
| 90 | + if has_running: |
| 91 | + new_status = Status.RUNNING |
| 92 | + else: |
| 93 | + has_failed = any(child.status == Status.FAILURE for child in self.children) |
| 94 | + if has_failed: |
| 95 | + new_status = Status.FAILURE |
| 96 | + else: |
| 97 | + new_status = Status.SUCCESS |
| 98 | + |
| 99 | + # If we've reached a final status, stop and terminate running children |
| 100 | + if new_status != Status.RUNNING: |
| 101 | + self.stop(new_status) |
| 102 | + |
| 103 | + self.status = new_status |
| 104 | + yield self |
23 | 105 |
|
24 | 106 |
|
25 | 107 | class FindWorkflowByUUID(FlagGuard): |
@@ -203,3 +285,109 @@ def __init__( |
203 | 285 | "extract_result_failed", |
204 | 286 | log_prefix=log_prefix, |
205 | 287 | ) |
| 288 | + |
| 289 | + |
| 290 | +class ResetPackageStateGuarded(FlagGuard): |
| 291 | + """ |
| 292 | + Reset package once if force_rebuild is True. |
| 293 | + Always returns SUCCESS. |
| 294 | + """ |
| 295 | + |
| 296 | + def __init__( |
| 297 | + self, |
| 298 | + name: str, |
| 299 | + package: Package, |
| 300 | + default_package: Package, |
| 301 | + log_prefix: str = "", |
| 302 | + ) -> None: |
| 303 | + super().__init__( |
| 304 | + None if name == "" else name, |
| 305 | + ResetPackageState( |
| 306 | + "Reset Package State", |
| 307 | + package, |
| 308 | + default_package, |
| 309 | + log_prefix=log_prefix, |
| 310 | + ), |
| 311 | + package.meta.ephemeral, |
| 312 | + "force_rebuild", |
| 313 | + flag_value=False, |
| 314 | + raise_on=[Status.SUCCESS, Status.FAILURE], |
| 315 | + guard_status=Status.SUCCESS, |
| 316 | + log_prefix=log_prefix, |
| 317 | + ) |
| 318 | + |
| 319 | + |
| 320 | +class RestartPackageGuarded(FlagGuard): |
| 321 | + """ |
| 322 | + Reset package if we didn't trigger the workflow in current run |
| 323 | + This is intended to be used for build workflow since if build has failed |
| 324 | + we have to reset not only build but also publish which effectively means |
| 325 | + we have to reset the entire package and restart from scratch. |
| 326 | +
|
| 327 | + When reset is made we return RUNNING to give the tree opportunity to run the workflow again. |
| 328 | + """ |
| 329 | + |
| 330 | + def __init__( |
| 331 | + self, |
| 332 | + name: str, |
| 333 | + package: Package, |
| 334 | + workflow: Workflow, |
| 335 | + default_package: Package, |
| 336 | + log_prefix: str = "", |
| 337 | + ) -> None: |
| 338 | + reset_package_state = ResetPackageState( |
| 339 | + "Reset Package State", |
| 340 | + package, |
| 341 | + default_package, |
| 342 | + log_prefix=log_prefix, |
| 343 | + ) |
| 344 | + reset_package_state_wrapped = SuccessIsRunning( |
| 345 | + "Success is Running", reset_package_state |
| 346 | + ) |
| 347 | + super().__init__( |
| 348 | + None if name == "" else name, |
| 349 | + reset_package_state_wrapped, |
| 350 | + workflow.ephemeral, |
| 351 | + "trigger_attempted", |
| 352 | + flag_value=True, |
| 353 | + raise_on=[], |
| 354 | + guard_status=Status.FAILURE, |
| 355 | + log_prefix=log_prefix, |
| 356 | + ) |
| 357 | + |
| 358 | + |
| 359 | +class RestartWorkflowGuarded(FlagGuard): |
| 360 | + """ |
| 361 | + Reset workflow if we didn't trigger the workflow in current run |
| 362 | +
|
| 363 | + This will only reset the workflow state |
| 364 | +
|
| 365 | + When reset is made we return RUNNING to give the tree opportunity to run the workflow again. |
| 366 | + """ |
| 367 | + |
| 368 | + def __init__( |
| 369 | + self, |
| 370 | + name: str, |
| 371 | + workflow: Workflow, |
| 372 | + default_workflow: Workflow, |
| 373 | + log_prefix: str = "", |
| 374 | + ) -> None: |
| 375 | + reset_workflow_state = ResetWorkflowState( |
| 376 | + "Reset Workflow State", |
| 377 | + workflow, |
| 378 | + default_workflow, |
| 379 | + log_prefix=log_prefix, |
| 380 | + ) |
| 381 | + reset_workflow_state_wrapped = SuccessIsRunning( |
| 382 | + "Success is Running", reset_workflow_state |
| 383 | + ) |
| 384 | + super().__init__( |
| 385 | + None if name == "" else name, |
| 386 | + reset_workflow_state_wrapped, |
| 387 | + workflow.ephemeral, |
| 388 | + "trigger_attempted", |
| 389 | + flag_value=True, |
| 390 | + raise_on=[], |
| 391 | + guard_status=Status.FAILURE, |
| 392 | + log_prefix=log_prefix, |
| 393 | + ) |
0 commit comments