Skip to content

Commit 62e0c65

Browse files
authored
Merge pull request #179 from chaostoolkit/chaostoolkit/issue176
Add rollback runtime strategies
2 parents 5dc8b58 + ee4b88f commit 62e0c65

File tree

8 files changed

+333
-7
lines changed

8 files changed

+333
-7
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@
44

55
[Unreleased]: https://github.com/chaostoolkit/chaostoolkit-lib/compare/1.10.0...HEAD
66

7+
### Added
8+
9+
- Added runtime strategies for rollback as per [chaostoolkit#176][].
10+
Until now, they were never played
11+
an activity would fail during the hypothesis or if the execution
12+
was interrupted from a control. With the strategies, you can now decide
13+
that they are always applied, never or only when the experiment deviated.
14+
This is a flag passed to the settings as follows:
15+
16+
```
17+
runtime:
18+
rollbacks:
19+
strategy: "always|never|default|deviated"
20+
```
21+
22+
The `"default"` strategy remains backward compatible.
23+
24+
[chaostoolkit#176]: https://github.com/chaostoolkit/chaostoolkit/issues/176
25+
726
## [1.10.0][] - 2020-06-19
827

928
[1.10.0]: https://github.com/chaostoolkit/chaostoolkit-lib/compare/1.9.0...1.10.0

chaoslib/experiment.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ def run_experiment(experiment: Experiment,
190190
initialize_global_controls(experiment, config, secrets, settings)
191191
initialize_controls(experiment, config, secrets)
192192
activity_pool, rollback_pool = get_background_pools(experiment)
193+
rollback_strategy = settings.get("runtime", {}).get(
194+
"rollbacks", {}).get("strategy", "default")
193195

194196
experiment["title"] = substitute(experiment["title"], config, secrets)
195197
logger.info("Running experiment: {t}".format(t=experiment["title"]))
@@ -254,6 +256,34 @@ def run_experiment(experiment: Experiment,
254256
"leaving without applying rollbacks.")
255257
else:
256258
journal["status"] = journal["status"] or "completed"
259+
260+
has_deviated = journal["deviated"]
261+
journal_status = journal["status"]
262+
play_rollbacks = False
263+
if rollback_strategy == "always":
264+
logger.warning(
265+
"Rollbacks were explicitly requested to be played")
266+
play_rollbacks = True
267+
elif rollback_strategy == "never":
268+
logger.warning(
269+
"Rollbacks were explicitly requested to not be played")
270+
play_rollbacks = False
271+
elif rollback_strategy == "default" and \
272+
journal_status not in ["failed", "interrupted"]:
273+
play_rollbacks = True
274+
elif rollback_strategy == "deviated":
275+
if has_deviated:
276+
logger.warning(
277+
"Rollbacks will be played only because the experiment "
278+
"deviated")
279+
play_rollbacks = True
280+
else:
281+
logger.warning(
282+
"Rollbacks werre explicitely requested to be played only "
283+
"if the experiment deviated. Since this is not the case, "
284+
"we will not play them.")
285+
286+
if play_rollbacks:
257287
try:
258288
journal["rollbacks"] = apply_rollbacks(
259289
experiment, config, secrets, rollback_pool, dry)
@@ -269,8 +299,7 @@ def run_experiment(experiment: Experiment,
269299
journal["end"] = datetime.utcnow().isoformat()
270300
journal["duration"] = time.time() - started_at
271301

272-
has_deviated = journal["deviated"]
273-
status = "deviated" if has_deviated else journal["status"]
302+
status = "deviated" if has_deviated else journal_status
274303

275304
logger.info(
276305
"Experiment ended with status: {s}".format(s=status))

tests/fixtures/actions.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,48 @@
1-
# -*- coding: utf-8 -*-
2-
import sys
1+
EmptyAction = {}
32

43

5-
EmptyAction = {}
4+
DoNothingAction = {
5+
"name": "a name",
6+
"type": "action",
7+
"provider": {
8+
"type": "python",
9+
"module": "fixtures.fakeext",
10+
"func": "do_nothing"
11+
}
12+
}
13+
14+
15+
EchoAction = {
16+
"name": "a name",
17+
"type": "action",
18+
"provider": {
19+
"type": "python",
20+
"module": "fixtures.fakeext",
21+
"func": "echo_message",
22+
"arguments": {
23+
"message": "kaboom"
24+
}
25+
}
26+
}
27+
28+
29+
FailAction = {
30+
"name": "a name",
31+
"type": "action",
32+
"provider": {
33+
"type": "python",
34+
"module": "fixtures.fakeext",
35+
"func": "force_failed_activity"
36+
}
37+
}
38+
39+
40+
InterruptAction = {
41+
"name": "a name",
42+
"type": "action",
43+
"provider": {
44+
"type": "python",
45+
"module": "fixtures.fakeext",
46+
"func": "force_interrupting_experiment"
47+
}
48+
}

tests/fixtures/experiments.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from copy import deepcopy
33
import os
44

5+
from fixtures.actions import DoNothingAction, EchoAction, FailAction, \
6+
InterruptAction
57
from fixtures.probes import BackgroundPythonModuleProbe, MissingFuncArgProbe, \
68
PythonModuleProbe, PythonModuleProbeWithBoolTolerance, \
79
PythonModuleProbeWithExternalTolerance, PythonModuleProbeWithLongPause, \
@@ -11,7 +13,7 @@
1113
PythonModuleProbeWithProcessStatusTolerance, \
1214
PythonModuleProbeWithProcessFailedStatusTolerance, \
1315
PythonModuleProbeWithProcesStdoutTolerance, \
14-
PythonModuleProbeWithHTTPStatusToleranceDeviation
16+
PythonModuleProbeWithHTTPStatusToleranceDeviation, FailProbe
1517

1618
Secrets = {}
1719

@@ -421,3 +423,75 @@
421423
path: {}
422424
timeout: 30
423425
""".format(os.path.abspath(__file__))
426+
427+
428+
ExperimentWithRegularRollback = {
429+
"title": "do cats live in the Internet?",
430+
"description": "an experiment of importance",
431+
"steady-state-hypothesis": {
432+
"title": "hello"
433+
},
434+
"method": [
435+
EchoAction
436+
],
437+
"rollbacks": [
438+
EchoAction
439+
]
440+
}
441+
442+
443+
ExperimentWithFailedActionInMethodAndARollback = {
444+
"title": "do cats live in the Internet?",
445+
"description": "an experiment of importance",
446+
"steady-state-hypothesis": {
447+
"title": "hello"
448+
},
449+
"method": [
450+
FailAction
451+
],
452+
"rollbacks": [
453+
EchoAction
454+
]
455+
}
456+
457+
458+
ExperimentWithFailedActionInSSHAndARollback = {
459+
"title": "do cats live in the Internet?",
460+
"description": "an experiment of importance",
461+
"steady-state-hypothesis": {
462+
"title": "hello",
463+
"probes": [
464+
FailProbe
465+
]
466+
},
467+
"method": [
468+
DoNothingAction
469+
],
470+
"rollbacks": [
471+
EchoAction
472+
]
473+
}
474+
475+
476+
ExperimentWithInterruptedExperimentAndARollback = {
477+
"title": "do cats live in the Internet?",
478+
"description": "an experiment of importance",
479+
"steady-state-hypothesis": {
480+
"title": "hello"
481+
},
482+
"method": [
483+
deepcopy(EchoAction)
484+
],
485+
"rollbacks": [
486+
EchoAction
487+
]
488+
}
489+
ExperimentWithInterruptedExperimentAndARollback["method"][0]["controls"] = [
490+
{
491+
"name": "dummy",
492+
"provider": {
493+
"type": "python",
494+
"module": "fixtures.interruptexperiment"
495+
}
496+
}
497+
]

tests/fixtures/fakeext.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
from typing import List
33

4-
from chaoslib.exceptions import InterruptExecution
4+
from chaoslib.exceptions import ActivityFailed, InterruptExecution
55

66
__all__ = ["many_args", "many_args_with_rich_types", "no_args_docstring",
77
"no_args", "one_arg", "one_untyped_arg", "one_arg_with_default",
@@ -66,3 +66,20 @@ def many_args_with_rich_types(message: str, recipients: List[str],
6666
Many arguments with rich types.
6767
"""
6868
pass
69+
70+
71+
def do_nothing():
72+
pass
73+
74+
75+
def echo_message(message: str) -> str:
76+
print(message)
77+
return message
78+
79+
80+
def force_failed_activity():
81+
raise ActivityFailed()
82+
83+
84+
def force_interrupting_experiment():
85+
raise InterruptExecution()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from chaoslib.exceptions import InterruptExecution
2+
3+
4+
def after_activity_control(**kwargs):
5+
raise InterruptExecution()

tests/fixtures/probes.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,14 @@ def must_be_in_range(a: int, b: int, value: Any = None) -> bool:
373373
raise ActivityFailed("body is not in expected range")
374374
else:
375375
return True
376+
377+
FailProbe = {
378+
"name": "a name",
379+
"type": "probe",
380+
"tolerance": True,
381+
"provider": {
382+
"type": "python",
383+
"module": "fixtures.fakeext",
384+
"func": "force_failed_activity"
385+
}
386+
}

0 commit comments

Comments
 (0)