ntswf 1.0.8 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +12 -34
- data/lib/ntswf/activity_worker.rb +50 -5
- data/lib/ntswf/base.rb +2 -1
- data/lib/ntswf/client.rb +116 -2
- data/lib/ntswf/decision_worker.rb +1 -3
- data/lib/ntswf/instance.rb +19 -0
- data/lib/ntswf/worker.rb +1 -1
- data/lib/ntswf.rb +16 -7
- metadata +3 -2
data/README.md
CHANGED
@@ -15,54 +15,32 @@ Usage
|
|
15
15
|
-----
|
16
16
|
### Gemfile
|
17
17
|
|
18
|
-
gem 'ntswf', '~>
|
18
|
+
gem 'ntswf', '~> 2.0'
|
19
19
|
|
20
20
|
### Client
|
21
21
|
```
|
22
|
-
class WorkflowClient
|
23
|
-
include Ntswf::Client
|
24
|
-
|
25
|
-
def enqueue!
|
26
|
-
start_execution(
|
27
|
-
execution_id: 'my_singleton_task',
|
28
|
-
name: 'my_worker_name',
|
29
|
-
params: {my: :param},
|
30
|
-
unit: 'my_worker',
|
31
|
-
)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
22
|
config = {domain: 'my_domain', unit: 'my_app'} # ...
|
36
|
-
|
23
|
+
Ntswf.create(:client, config).start_execution(
|
24
|
+
execution_id: 'my_singleton_task',
|
25
|
+
name: 'my_worker_name',
|
26
|
+
params: {my_param: :param},
|
27
|
+
unit: 'my_worker',
|
28
|
+
)
|
37
29
|
```
|
38
|
-
See {Ntswf::Base#
|
30
|
+
See {Ntswf::Base#configure} for configuration options.
|
39
31
|
|
40
32
|
### Decision worker
|
41
33
|
```
|
42
|
-
class Decider
|
43
|
-
include Ntswf::DecisionWorker
|
44
|
-
end
|
45
|
-
|
46
34
|
config = {domain: 'my_domain', unit: 'my_app'} # ...
|
47
|
-
|
35
|
+
Ntswf.create(:decision_worker, config).process_decisions
|
48
36
|
```
|
49
37
|
|
50
38
|
### Activity worker
|
51
39
|
```
|
52
|
-
class Worker
|
53
|
-
include Ntswf::ActivityWorker
|
54
|
-
|
55
|
-
def process_activity_task
|
56
|
-
super do |task|
|
57
|
-
options = parse_input(task.input)
|
58
|
-
# ...
|
59
|
-
task.complete!(result: 'OK')
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
40
|
config = {domain: 'my_domain', unit: 'my_worker'} # ...
|
65
|
-
|
41
|
+
worker = Ntswf.create(:activity_worker, config)
|
42
|
+
worker.on_task ->(task) { Ntswf.result task.params['my_param'] }
|
43
|
+
worker.process_activities
|
66
44
|
```
|
67
45
|
|
68
46
|
### Setup helpers
|
@@ -5,6 +5,29 @@ module Ntswf
|
|
5
5
|
module ActivityWorker
|
6
6
|
include Ntswf::Worker
|
7
7
|
|
8
|
+
# Configure a proc or block to be called on receiving an {AWS::SimpleWorkflow::ActivityTask}
|
9
|
+
# @yieldparam task [Hash]
|
10
|
+
# Description of the task's properties:
|
11
|
+
# :activity_task:: The {AWS::SimpleWorkflow::ActivityTask}
|
12
|
+
# :name:: Kind of task
|
13
|
+
# :params:: Custom parameters given to the execution (parsed back from JSON)
|
14
|
+
# :version:: Client version
|
15
|
+
#
|
16
|
+
# See {Ntswf::Client#start_execution}'s options for details
|
17
|
+
# @param proc [Proc] The callback
|
18
|
+
# @yieldreturn [Hash]
|
19
|
+
# Processing result. The following keys are interpreted accordingly:
|
20
|
+
# :error:: Fails the task with the given error details.
|
21
|
+
# :outcome:: Completes the task, storing the outcome's value (as JSON).
|
22
|
+
# :seconds_until_retry::
|
23
|
+
# Re-schedules the task after the given delay.
|
24
|
+
# In combination with an *:error*: Marks the task for immediate re-scheduling,
|
25
|
+
# ignoring the value.
|
26
|
+
# Please note that the behaviour is undefined if an *:interval* option has been set.
|
27
|
+
def on_activity(proc = nil, &block)
|
28
|
+
@task_callback = proc || block
|
29
|
+
end
|
30
|
+
|
8
31
|
# Start the worker loop for activity tasks.
|
9
32
|
def process_activities
|
10
33
|
loop { in_subprocess :process_activity_task }
|
@@ -12,19 +35,41 @@ module Ntswf
|
|
12
35
|
|
13
36
|
def process_activity_task
|
14
37
|
announce("polling for activity task #{activity_task_list}")
|
15
|
-
domain.activity_tasks.poll_for_single_task(activity_task_list) do |
|
16
|
-
announce("got activity task #{
|
38
|
+
domain.activity_tasks.poll_for_single_task(activity_task_list) do |activity_task|
|
39
|
+
announce("got activity task #{activity_task.activity_type.inspect} #{activity_task.input}")
|
17
40
|
begin
|
18
|
-
|
41
|
+
returned_hash = @task_callback.call(describe(activity_task)) if @task_callback
|
42
|
+
process_returned_hash(activity_task, returned_hash)
|
19
43
|
rescue => e
|
20
|
-
notify(e, activity_type:
|
44
|
+
notify(e, activity_type: activity_task.activity_type.inspect, input: activity_task.input)
|
21
45
|
details = {
|
22
46
|
error: e.message[0, 1000],
|
23
47
|
exception: e.class.to_s[0, 1000],
|
24
48
|
}
|
25
|
-
|
49
|
+
activity_task.fail!(details: details.to_json, reason: 'Exception')
|
26
50
|
end
|
27
51
|
end
|
28
52
|
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def describe(activity_task)
|
57
|
+
options = parse_input(activity_task.input).merge(activity_task: activity_task)
|
58
|
+
options.map { |k, v| {k.to_sym => v} }.reduce(&:merge!)
|
59
|
+
end
|
60
|
+
|
61
|
+
KNOWN_RETURN_KEYS = [:error, :outcome, :seconds_until_retry]
|
62
|
+
|
63
|
+
def process_returned_hash(activity_task, returned_hash)
|
64
|
+
return unless returned_hash.kind_of? Hash
|
65
|
+
kind, value = returned_hash.detect { |k, v| KNOWN_RETURN_KEYS.include? k }
|
66
|
+
case kind
|
67
|
+
when :error
|
68
|
+
reason = returned_hash[:seconds_until_retry] ? "Retry" : "Error"
|
69
|
+
activity_task.fail!(details: {error: value.to_s[0, 1000]}.to_json, reason: reason)
|
70
|
+
when :outcome, :seconds_until_retry
|
71
|
+
activity_task.complete!(result: returned_hash.to_json)
|
72
|
+
end
|
73
|
+
end
|
29
74
|
end
|
30
75
|
end
|
data/lib/ntswf/base.rb
CHANGED
@@ -12,7 +12,8 @@ module Ntswf
|
|
12
12
|
# @option config [Numeric] :subprocess_retries (0) see {Worker#in_subprocess}
|
13
13
|
# @option config [String] :secret_access_key AWS credential
|
14
14
|
# @option config [String] :unit This worker/client's activity task list key
|
15
|
-
|
15
|
+
# @raise If a task list name is invalid
|
16
|
+
def configure(config)
|
16
17
|
@config = OpenStruct.new(config)
|
17
18
|
raise_if_invalid_task_list
|
18
19
|
end
|
data/lib/ntswf/client.rb
CHANGED
@@ -22,11 +22,11 @@ module Ntswf
|
|
22
22
|
# The executing unit's key, a corresponding activity task list must be configured
|
23
23
|
# @option options [Numeric] :version
|
24
24
|
# Optional minimum version of the client. The task may be rescheduled by older clients.
|
25
|
-
# @return
|
25
|
+
# @return (see #find)
|
26
26
|
# @raise [AWS::SimpleWorkflow::Errors::WorkflowExecutionAlreadyStartedFault]
|
27
27
|
def start_execution(options)
|
28
28
|
execution_id = options.delete(:execution_id)
|
29
|
-
workflow_type.start_execution(
|
29
|
+
workflow_execution = workflow_type.start_execution(
|
30
30
|
child_policy: :terminate,
|
31
31
|
execution_start_to_close_timeout: 48 * 3600,
|
32
32
|
input: options.to_json,
|
@@ -35,10 +35,124 @@ module Ntswf
|
|
35
35
|
task_start_to_close_timeout: 10 * 60,
|
36
36
|
workflow_id: [activity_task_list, execution_id].join(separator),
|
37
37
|
)
|
38
|
+
execution_details(workflow_execution).merge!(
|
39
|
+
name: options[:name].to_s,
|
40
|
+
params: options[:params],
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Get status and details of a workflow execution.
|
45
|
+
# @param ids [Hash] Identifies the queried execution
|
46
|
+
# @option ids [String] :workflow_id Workflow ID
|
47
|
+
# @option ids [String] :run_id Run ID
|
48
|
+
# @raise [AWS::SimpleWorkflow::Errors::UnknownResourceFault]
|
49
|
+
# @return [Hash]
|
50
|
+
# Execution properties.
|
51
|
+
# :exception:: Exception message for an unexpectedly failed execution
|
52
|
+
# :error:: Error message returned from an execution
|
53
|
+
# :outcome:: Result of a completed execution
|
54
|
+
# :params:: Custom params from JSON
|
55
|
+
# :run_id:: The workflow execution's run ID
|
56
|
+
# :status:: Calculated workflow execution status (:completed, :open, others indicating failure)
|
57
|
+
# :name:: Given task kind
|
58
|
+
# :workflow_id:: The workflow execution's workflow ID
|
59
|
+
def find(ids)
|
60
|
+
workflow_execution = domain.workflow_executions.at(ids[:workflow_id], ids[:run_id])
|
61
|
+
history_details(workflow_execution)
|
38
62
|
end
|
39
63
|
|
40
64
|
protected
|
41
65
|
|
66
|
+
def execution_details(workflow_execution)
|
67
|
+
{
|
68
|
+
workflow_id: workflow_execution.workflow_id,
|
69
|
+
run_id: workflow_execution.run_id,
|
70
|
+
status: workflow_execution.status,
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def history_details(workflow_execution)
|
75
|
+
result = execution_details(workflow_execution)
|
76
|
+
input = parse_input workflow_execution.history_events.first.attributes.input
|
77
|
+
result.merge!(name: input["name"].to_s, params: input["params"])
|
78
|
+
|
79
|
+
case result[:status]
|
80
|
+
when :open
|
81
|
+
# nothing
|
82
|
+
when :completed
|
83
|
+
result.merge!(completion_details workflow_execution)
|
84
|
+
else
|
85
|
+
result.merge!(failure_details workflow_execution)
|
86
|
+
end
|
87
|
+
result
|
88
|
+
end
|
89
|
+
|
90
|
+
def completion_details(workflow_execution)
|
91
|
+
completed_event = workflow_execution.history_events.reverse_order.detect do |e|
|
92
|
+
e.event_type == "WorkflowExecutionCompleted"
|
93
|
+
end
|
94
|
+
if completed_event
|
95
|
+
{outcome: parse_attribute(completed_event, :result)["outcome"]}
|
96
|
+
else
|
97
|
+
{status: :open}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
TERMINAL_EVENT_TYPES_ON_FAILURE = %w(
|
102
|
+
WorkflowExecutionFailed
|
103
|
+
WorkflowExecutionTimedOut
|
104
|
+
WorkflowExecutionCanceled
|
105
|
+
WorkflowExecutionTerminated
|
106
|
+
)
|
107
|
+
|
108
|
+
def failure_details(workflow_execution)
|
109
|
+
terminal_event = workflow_execution.history_events.reverse_order.detect {|e|
|
110
|
+
TERMINAL_EVENT_TYPES_ON_FAILURE.include?(e.event_type)
|
111
|
+
}
|
112
|
+
if terminal_event
|
113
|
+
event_type = terminal_event.event_type
|
114
|
+
case event_type
|
115
|
+
when "WorkflowExecutionFailed"
|
116
|
+
details = parse_attribute(terminal_event, :details)
|
117
|
+
{
|
118
|
+
error: details["error"],
|
119
|
+
exception: details["exception"],
|
120
|
+
}
|
121
|
+
else
|
122
|
+
{
|
123
|
+
error: event_type,
|
124
|
+
exception: event_type,
|
125
|
+
}
|
126
|
+
end
|
127
|
+
else
|
128
|
+
log("No terminal event for execution"\
|
129
|
+
" #{workflow_execution.workflow_id} | #{workflow_execution.run_id}."\
|
130
|
+
" Event types: #{workflow_execution.history_events.map(&:event_type)}") rescue nil
|
131
|
+
{
|
132
|
+
error: "Execution has finished with status #{workflow_execution.status},"\
|
133
|
+
" but did not provide details."
|
134
|
+
}
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def parse_attribute(event, key)
|
139
|
+
value = nil
|
140
|
+
begin
|
141
|
+
json_value = event.attributes[key]
|
142
|
+
rescue ArgumentError
|
143
|
+
# missing key in event attributes
|
144
|
+
end
|
145
|
+
if json_value
|
146
|
+
begin
|
147
|
+
value = JSON.parse json_value
|
148
|
+
rescue # JSON::ParserError, ...
|
149
|
+
# no JSON
|
150
|
+
end
|
151
|
+
end
|
152
|
+
value = nil unless value.kind_of? Hash
|
153
|
+
value || {}
|
154
|
+
end
|
155
|
+
|
42
156
|
def workflow_type
|
43
157
|
@workflow_type ||= domain.workflow_types[workflow_name, workflow_version]
|
44
158
|
end
|
@@ -16,9 +16,7 @@ module Ntswf
|
|
16
16
|
# reason:: reschedule if {RETRY}
|
17
17
|
# result:: Interpreted as {Hash}, see below for keys
|
18
18
|
# Result keys
|
19
|
-
# :seconds_until_retry::
|
20
|
-
# Planned re-schedule after task completion. Please note that
|
21
|
-
# given an *:interval* option the behaviour of this key is undefined
|
19
|
+
# :seconds_until_retry:: See {ActivityWorker#on_activity}
|
22
20
|
def process_decision_task
|
23
21
|
announce("polling for decision task #{decision_task_list}")
|
24
22
|
domain.decision_tasks.poll_for_single_task(decision_task_list) do |task|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Ntswf
|
2
|
+
class Instance
|
3
|
+
# @!method initialize(*modules, config)
|
4
|
+
# @param modules (DEFAULT_MODULES)
|
5
|
+
# A list of module names to include
|
6
|
+
# @param config (see Base#configure)
|
7
|
+
# @option config (see Base#configure)
|
8
|
+
def initialize(*args)
|
9
|
+
symbols = args.grep Symbol
|
10
|
+
configs = args - symbols
|
11
|
+
instance_exec do
|
12
|
+
module_names = symbols.map(&:to_s).map { |s| s.gsub(/(^|_)(.)/) { $2.upcase } }
|
13
|
+
module_names = DEFAULT_MODULES if module_names.empty?
|
14
|
+
module_names.each { |module_name| extend Ntswf::const_get module_name }
|
15
|
+
end
|
16
|
+
configure(configs.last || {})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/ntswf/worker.rb
CHANGED
@@ -7,7 +7,7 @@ module Ntswf
|
|
7
7
|
# *reason* value to force task reschedule, may be set if the worker is unable process the task
|
8
8
|
RETRY = "Retry"
|
9
9
|
|
10
|
-
# Run a method in a separate
|
10
|
+
# Run a method in a separate process.
|
11
11
|
# This will ensure the call lives on if the master process is terminated.
|
12
12
|
# If the *:subprocess_retries* configuration is set {StandardError}s during the
|
13
13
|
# method call will be retried accordingly.
|
data/lib/ntswf.rb
CHANGED
@@ -1,16 +1,25 @@
|
|
1
1
|
module Ntswf
|
2
|
-
|
2
|
+
# @!method self.create(*modules, config)
|
3
|
+
# Shortcut for creating an {Instance}
|
4
|
+
# @example
|
5
|
+
# Ntswf.create(:client, :activity_worker, unit: "my_worker")
|
6
|
+
# @param modules (see Instance#initialize)
|
7
|
+
# @param config (see Instance#initialize)
|
8
|
+
# @option config (see Base#configure)
|
9
|
+
def self.create(*args)
|
10
|
+
Ntswf::Instance.new(*args)
|
11
|
+
end
|
12
|
+
|
13
|
+
DEFAULT_MODULES = %w(
|
3
14
|
ActivityWorker
|
4
15
|
Client
|
5
16
|
DecisionWorker
|
6
17
|
Utils
|
7
18
|
)
|
8
19
|
|
9
|
-
AUTOLOAD
|
20
|
+
AUTOLOAD = DEFAULT_MODULES + %w(
|
21
|
+
Instance
|
22
|
+
)
|
10
23
|
|
11
|
-
|
12
|
-
base.module_exec do
|
13
|
-
AUTOLOAD.each { |c| include const_get c }
|
14
|
-
end
|
15
|
-
end
|
24
|
+
AUTOLOAD.each { |c| autoload c.to_sym, "ntswf/#{c.gsub(/.(?=[A-Z])/, '\0_').downcase}.rb" }
|
16
25
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ntswf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-
|
12
|
+
date: 2014-02-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: aws-sdk
|
@@ -54,6 +54,7 @@ files:
|
|
54
54
|
- lib/ntswf/decision_worker.rb
|
55
55
|
- lib/ntswf/utils.rb
|
56
56
|
- lib/ntswf/activity_worker.rb
|
57
|
+
- lib/ntswf/instance.rb
|
57
58
|
- lib/ntswf/worker.rb
|
58
59
|
- lib/ntswf/client.rb
|
59
60
|
- lib/ntswf/base.rb
|