Add some basic checks on SWF decisions, more to come later
This commit is contained in:
parent
381eb5eb0f
commit
0749b30fb4
@ -87,3 +87,35 @@ class SWFValidationException(SWFClientError):
|
||||
message,
|
||||
"com.amazon.coral.validate#ValidationException"
|
||||
)
|
||||
|
||||
|
||||
class SWFDecisionValidationException(SWFClientError):
|
||||
def __init__(self, problems):
|
||||
# messages
|
||||
messages = []
|
||||
for pb in problems:
|
||||
if pb["type"] == "null_value":
|
||||
messages.append(
|
||||
"Value null at '%(where)s' failed to satisfy constraint: "\
|
||||
"Member must not be null" % pb
|
||||
)
|
||||
elif pb["type"] == "bad_decision_type":
|
||||
messages.append(
|
||||
"Value '%(value)s' at '%(where)s' failed to satisfy constraint: " \
|
||||
"Member must satisfy enum value set: " \
|
||||
"[%(possible_values)s]" % pb
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Unhandled decision constraint type: {}".format(pb["type"])
|
||||
)
|
||||
# prefix
|
||||
count = len(problems)
|
||||
if count < 2:
|
||||
prefix = "{} validation error detected:"
|
||||
else:
|
||||
prefix = "{} validation errors detected:"
|
||||
super(SWFDecisionValidationException, self).__init__(
|
||||
prefix.format(count) + "; ".join(messages),
|
||||
"com.amazon.coral.validate#ValidationException"
|
||||
)
|
||||
|
@ -3,12 +3,36 @@ import uuid
|
||||
|
||||
from moto.core.utils import camelcase_to_underscores
|
||||
|
||||
from ..exceptions import SWFDefaultUndefinedFault
|
||||
from ..exceptions import (
|
||||
SWFDefaultUndefinedFault,
|
||||
SWFValidationException,
|
||||
SWFDecisionValidationException,
|
||||
)
|
||||
from .decision_task import DecisionTask
|
||||
from .history_event import HistoryEvent
|
||||
|
||||
|
||||
# TODO: extract decision related logic into a Decision class
|
||||
class WorkflowExecution(object):
|
||||
|
||||
# NB: the list is ordered exactly as in SWF validation exceptions so we can
|
||||
# mimic error messages closely ; don't reorder it without checking SWF.
|
||||
KNOWN_DECISION_TYPES = [
|
||||
"CompleteWorkflowExecution",
|
||||
"StartTimer",
|
||||
"RequestCancelExternalWorkflowExecution",
|
||||
"SignalExternalWorkflowExecution",
|
||||
"CancelTimer",
|
||||
"RecordMarker",
|
||||
"ScheduleActivityTask",
|
||||
"ContinueAsNewWorkflowExecution",
|
||||
"ScheduleLambdaFunction",
|
||||
"FailWorkflowExecution",
|
||||
"RequestCancelActivityTask",
|
||||
"StartChildWorkflowExecution",
|
||||
"CancelWorkflowExecution"
|
||||
]
|
||||
|
||||
def __init__(self, workflow_type, workflow_id, **kwargs):
|
||||
self.workflow_type = workflow_type
|
||||
self.workflow_id = workflow_id
|
||||
@ -154,6 +178,7 @@ class WorkflowExecution(object):
|
||||
|
||||
def complete_decision_task(self, task_token, decisions=None, execution_context=None):
|
||||
# TODO: check if decision can really complete in case of malformed "decisions"
|
||||
self.validate_decisions(decisions)
|
||||
dt = self._find_decision_task(task_token)
|
||||
evt = self._add_event(
|
||||
"DecisionTaskCompleted",
|
||||
@ -164,6 +189,48 @@ class WorkflowExecution(object):
|
||||
dt.complete()
|
||||
self.handle_decisions(evt.event_id, decisions)
|
||||
|
||||
def validate_decisions(self, decisions):
|
||||
"""
|
||||
Performs some basic validations on decisions. The real SWF service
|
||||
seems to break early and *not* process any decision if there's a
|
||||
validation problem, such as a malformed decision for instance. I didn't
|
||||
find an explicit documentation for that though, so criticisms welcome.
|
||||
"""
|
||||
if not decisions:
|
||||
return
|
||||
|
||||
problems = []
|
||||
|
||||
# check close decision is last
|
||||
# TODO: see what happens on real SWF service if we ask for 2 close decisions
|
||||
for dcs in decisions[:-1]:
|
||||
close_decision_types = [
|
||||
"CompleteWorkflowExecution",
|
||||
"FailWorkflowExecution",
|
||||
"CancelWorkflowExecution",
|
||||
]
|
||||
if dcs["decisionType"] in close_decision_types:
|
||||
raise SWFValidationException(
|
||||
"Close must be last decision in list"
|
||||
)
|
||||
|
||||
decision_number = 0
|
||||
for dcs in decisions:
|
||||
decision_number += 1
|
||||
# TODO: check decision types mandatory attributes
|
||||
# check decision type is correct
|
||||
if dcs["decisionType"] not in self.KNOWN_DECISION_TYPES:
|
||||
problems.append({
|
||||
"type": "bad_decision_type",
|
||||
"value": dcs["decisionType"],
|
||||
"where": "decisions.{}.member.decisionType".format(decision_number),
|
||||
"possible_values": ", ".join(self.KNOWN_DECISION_TYPES),
|
||||
})
|
||||
|
||||
# raise if any problem
|
||||
if any(problems):
|
||||
raise SWFDecisionValidationException(problems)
|
||||
|
||||
def handle_decisions(self, event_id, decisions):
|
||||
"""
|
||||
Handles a Decision according to SWF docs.
|
||||
|
@ -6,6 +6,7 @@ from moto.swf import swf_backend
|
||||
from moto.swf.exceptions import (
|
||||
SWFUnknownResourceFault,
|
||||
SWFValidationException,
|
||||
SWFDecisionValidationException,
|
||||
)
|
||||
|
||||
from .utils import mock_basic_workflow_type
|
||||
@ -157,3 +158,36 @@ def test_respond_decision_task_completed_with_complete_workflow_execution():
|
||||
"DecisionTaskCompleted",
|
||||
"WorkflowExecutionCompleted",
|
||||
])
|
||||
|
||||
@mock_swf
|
||||
def test_respond_decision_task_completed_with_close_decision_not_last():
|
||||
conn = setup_workflow()
|
||||
resp = conn.poll_for_decision_task("test-domain", "queue")
|
||||
task_token = resp["taskToken"]
|
||||
|
||||
decisions = [
|
||||
{ "decisionType": "CompleteWorkflowExecution" },
|
||||
{ "decisionType": "WeDontCare" },
|
||||
]
|
||||
|
||||
conn.respond_decision_task_completed.when.called_with(
|
||||
task_token, decisions=decisions
|
||||
).should.throw(SWFValidationException, r"Close must be last decision in list")
|
||||
|
||||
@mock_swf
|
||||
def test_respond_decision_task_completed_with_invalid_decision_type():
|
||||
conn = setup_workflow()
|
||||
resp = conn.poll_for_decision_task("test-domain", "queue")
|
||||
task_token = resp["taskToken"]
|
||||
|
||||
decisions = [
|
||||
{ "decisionType": "BadDecisionType" },
|
||||
{ "decisionType": "CompleteWorkflowExecution" },
|
||||
]
|
||||
|
||||
conn.respond_decision_task_completed.when.called_with(
|
||||
task_token, decisions=decisions
|
||||
).should.throw(
|
||||
SWFDecisionValidationException,
|
||||
r"Value 'BadDecisionType' at 'decisions.1.member.decisionType'"
|
||||
)
|
||||
|
@ -11,6 +11,7 @@ from moto.swf.exceptions import (
|
||||
SWFWorkflowExecutionAlreadyStartedFault,
|
||||
SWFDefaultUndefinedFault,
|
||||
SWFValidationException,
|
||||
SWFDecisionValidationException,
|
||||
)
|
||||
from moto.swf.models import (
|
||||
WorkflowType,
|
||||
@ -124,3 +125,28 @@ def test_swf_validation_exception():
|
||||
"__type": "com.amazon.coral.validate#ValidationException",
|
||||
"message": "Invalid token",
|
||||
})
|
||||
|
||||
def test_swf_decision_validation_error():
|
||||
ex = SWFDecisionValidationException([
|
||||
{ "type": "null_value",
|
||||
"where": "decisions.1.member.startTimerDecisionAttributes.startToFireTimeout" },
|
||||
{ "type": "bad_decision_type",
|
||||
"value": "FooBar",
|
||||
"where": "decisions.1.member.decisionType",
|
||||
"possible_values": "Foo, Bar, Baz"},
|
||||
])
|
||||
|
||||
ex.status.should.equal(400)
|
||||
ex.error_code.should.equal("ValidationException")
|
||||
ex.body["__type"].should.equal("com.amazon.coral.validate#ValidationException")
|
||||
|
||||
msg = ex.body["message"]
|
||||
msg.should.match(r"^2 validation errors detected:")
|
||||
msg.should.match(
|
||||
r"Value null at 'decisions.1.member.startTimerDecisionAttributes.startToFireTimeout' "\
|
||||
r"failed to satisfy constraint: Member must not be null;"
|
||||
)
|
||||
msg.should.match(
|
||||
r"Value 'FooBar' at 'decisions.1.member.decisionType' failed to satisfy constraint: " \
|
||||
r"Member must satisfy enum value set: \[Foo, Bar, Baz\]"
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user