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 CHANGED
@@ -15,54 +15,32 @@ Usage
15
15
  -----
16
16
  ### Gemfile
17
17
 
18
- gem 'ntswf', '~> 1.0'
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
- WorkflowClient.new(config).enqueue!
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#initialize} for configuration options.
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
- Decider.new(config).process_decisions
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
- Worker.new(config).process_activities
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 |task|
16
- announce("got activity task #{task.activity_type.inspect} #{task.input}")
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
- yield task
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: task.activity_type.inspect, input: task.input)
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
- task.fail!(details: details.to_json, reason: 'Exception')
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
- def initialize(config)
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 [AWS::SimpleWorkflow::WorkflowExecution]
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 thread.
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
- AUTOLOAD = %w(
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.each { |c| autoload c.to_sym, "ntswf/#{c.gsub(/.(?=[A-Z])/, '\0_').downcase}.rb" }
20
+ AUTOLOAD = DEFAULT_MODULES + %w(
21
+ Instance
22
+ )
10
23
 
11
- def self.included(base)
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: 1.0.8
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-01-28 00:00:00.000000000 Z
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