ntswf 1.0.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|