Add some basic checks on SWF decisions, more to come later

This commit is contained in:
Jean-Baptiste Barth 2015-10-19 00:09:51 +02:00
parent 381eb5eb0f
commit 0749b30fb4
4 changed files with 160 additions and 1 deletions

View File

@ -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"
)

View File

@ -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.

View File

@ -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'"
)

View File

@ -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\]"
)