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