ntswf 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,165 @@
1
+ GNU LESSER GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+
9
+ This version of the GNU Lesser General Public License incorporates
10
+ the terms and conditions of version 3 of the GNU General Public
11
+ License, supplemented by the additional permissions listed below.
12
+
13
+ 0. Additional Definitions.
14
+
15
+ As used herein, "this License" refers to version 3 of the GNU Lesser
16
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
17
+ General Public License.
18
+
19
+ "The Library" refers to a covered work governed by this License,
20
+ other than an Application or a Combined Work as defined below.
21
+
22
+ An "Application" is any work that makes use of an interface provided
23
+ by the Library, but which is not otherwise based on the Library.
24
+ Defining a subclass of a class defined by the Library is deemed a mode
25
+ of using an interface provided by the Library.
26
+
27
+ A "Combined Work" is a work produced by combining or linking an
28
+ Application with the Library. The particular version of the Library
29
+ with which the Combined Work was made is also called the "Linked
30
+ Version".
31
+
32
+ The "Minimal Corresponding Source" for a Combined Work means the
33
+ Corresponding Source for the Combined Work, excluding any source code
34
+ for portions of the Combined Work that, considered in isolation, are
35
+ based on the Application, and not on the Linked Version.
36
+
37
+ The "Corresponding Application Code" for a Combined Work means the
38
+ object code and/or source code for the Application, including any data
39
+ and utility programs needed for reproducing the Combined Work from the
40
+ Application, but excluding the System Libraries of the Combined Work.
41
+
42
+ 1. Exception to Section 3 of the GNU GPL.
43
+
44
+ You may convey a covered work under sections 3 and 4 of this License
45
+ without being bound by section 3 of the GNU GPL.
46
+
47
+ 2. Conveying Modified Versions.
48
+
49
+ If you modify a copy of the Library, and, in your modifications, a
50
+ facility refers to a function or data to be supplied by an Application
51
+ that uses the facility (other than as an argument passed when the
52
+ facility is invoked), then you may convey a copy of the modified
53
+ version:
54
+
55
+ a) under this License, provided that you make a good faith effort to
56
+ ensure that, in the event an Application does not supply the
57
+ function or data, the facility still operates, and performs
58
+ whatever part of its purpose remains meaningful, or
59
+
60
+ b) under the GNU GPL, with none of the additional permissions of
61
+ this License applicable to that copy.
62
+
63
+ 3. Object Code Incorporating Material from Library Header Files.
64
+
65
+ The object code form of an Application may incorporate material from
66
+ a header file that is part of the Library. You may convey such object
67
+ code under terms of your choice, provided that, if the incorporated
68
+ material is not limited to numerical parameters, data structure
69
+ layouts and accessors, or small macros, inline functions and templates
70
+ (ten or fewer lines in length), you do both of the following:
71
+
72
+ a) Give prominent notice with each copy of the object code that the
73
+ Library is used in it and that the Library and its use are
74
+ covered by this License.
75
+
76
+ b) Accompany the object code with a copy of the GNU GPL and this license
77
+ document.
78
+
79
+ 4. Combined Works.
80
+
81
+ You may convey a Combined Work under terms of your choice that,
82
+ taken together, effectively do not restrict modification of the
83
+ portions of the Library contained in the Combined Work and reverse
84
+ engineering for debugging such modifications, if you also do each of
85
+ the following:
86
+
87
+ a) Give prominent notice with each copy of the Combined Work that
88
+ the Library is used in it and that the Library and its use are
89
+ covered by this License.
90
+
91
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
92
+ document.
93
+
94
+ c) For a Combined Work that displays copyright notices during
95
+ execution, include the copyright notice for the Library among
96
+ these notices, as well as a reference directing the user to the
97
+ copies of the GNU GPL and this license document.
98
+
99
+ d) Do one of the following:
100
+
101
+ 0) Convey the Minimal Corresponding Source under the terms of this
102
+ License, and the Corresponding Application Code in a form
103
+ suitable for, and under terms that permit, the user to
104
+ recombine or relink the Application with a modified version of
105
+ the Linked Version to produce a modified Combined Work, in the
106
+ manner specified by section 6 of the GNU GPL for conveying
107
+ Corresponding Source.
108
+
109
+ 1) Use a suitable shared library mechanism for linking with the
110
+ Library. A suitable mechanism is one that (a) uses at run time
111
+ a copy of the Library already present on the user's computer
112
+ system, and (b) will operate properly with a modified version
113
+ of the Library that is interface-compatible with the Linked
114
+ Version.
115
+
116
+ e) Provide Installation Information, but only if you would otherwise
117
+ be required to provide such information under section 6 of the
118
+ GNU GPL, and only to the extent that such information is
119
+ necessary to install and execute a modified version of the
120
+ Combined Work produced by recombining or relinking the
121
+ Application with a modified version of the Linked Version. (If
122
+ you use option 4d0, the Installation Information must accompany
123
+ the Minimal Corresponding Source and Corresponding Application
124
+ Code. If you use option 4d1, you must provide the Installation
125
+ Information in the manner specified by section 6 of the GNU GPL
126
+ for conveying Corresponding Source.)
127
+
128
+ 5. Combined Libraries.
129
+
130
+ You may place library facilities that are a work based on the
131
+ Library side by side in a single library together with other library
132
+ facilities that are not Applications and are not covered by this
133
+ License, and convey such a combined library under terms of your
134
+ choice, if you do both of the following:
135
+
136
+ a) Accompany the combined library with a copy of the same work based
137
+ on the Library, uncombined with any other library facilities,
138
+ conveyed under the terms of this License.
139
+
140
+ b) Give prominent notice with the combined library that part of it
141
+ is a work based on the Library, and explaining where to find the
142
+ accompanying uncombined form of the same work.
143
+
144
+ 6. Revised Versions of the GNU Lesser General Public License.
145
+
146
+ The Free Software Foundation may publish revised and/or new versions
147
+ of the GNU Lesser General Public License from time to time. Such new
148
+ versions will be similar in spirit to the present version, but may
149
+ differ in detail to address new problems or concerns.
150
+
151
+ Each version is given a distinguishing version number. If the
152
+ Library as you received it specifies that a certain numbered version
153
+ of the GNU Lesser General Public License "or any later version"
154
+ applies to it, you have the option of following the terms and
155
+ conditions either of that published version or of any later version
156
+ published by the Free Software Foundation. If the Library as you
157
+ received it does not specify a version number of the GNU Lesser
158
+ General Public License, you may choose any version of the GNU Lesser
159
+ General Public License ever published by the Free Software Foundation.
160
+
161
+ If the Library as you received it specifies that a proxy can decide
162
+ whether future versions of the GNU Lesser General Public License shall
163
+ apply, that proxy's public statement of acceptance of any version is
164
+ permanent authorization for you to choose that version for the
165
+ Library.
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ NTSWF
2
+ =====
3
+
4
+ Not That Simple Workflow - A layer around AWS SWF
5
+
6
+ Common denominator of our [infopark](http://infopark.com/) internal services using
7
+ AWS Simple Workflow.
8
+
9
+ Usage
10
+ -----
11
+ ### Gemfile
12
+
13
+ gem 'ntswf', '~> 1.0'
14
+
15
+ ### Client
16
+ ```
17
+ class WorkflowClient
18
+ include Ntswf::Client
19
+
20
+ def enqueue!
21
+ start_execution(
22
+ execution_id: 'my_singleton_task',
23
+ name: 'my_worker_name',
24
+ params: {my: :param},
25
+ unit: 'my_worker',
26
+ )
27
+ end
28
+ end
29
+
30
+ config = {domain: 'my_domain', unit: 'my_app'} # ...
31
+ WorkflowClient.new(config).enqueue!
32
+ ```
33
+ See {Ntswf::Base#initialize} for configuration options.
34
+
35
+ ### Decision worker
36
+ ```
37
+ class Decider
38
+ include Ntswf::DecisionWorker
39
+ end
40
+
41
+ config = {domain: 'my_domain', unit: 'my_app'} # ...
42
+ loop { Decider.new(config).process_decision_task }
43
+ ```
44
+
45
+ ### Activity worker
46
+ ```
47
+ class Worker
48
+ include Ntswf::ActivityWorker
49
+
50
+ def process_activity_task
51
+ super do |task|
52
+ options = parse_input(task.input)
53
+ # ...
54
+ task.complete!(result: 'OK')
55
+ end
56
+ end
57
+ end
58
+
59
+ config = {domain: 'my_domain', unit: 'my_worker'} # ...
60
+ loop { Worker.new(config).process_activity_task }
61
+ ```
62
+
63
+ ### Setup helpers
64
+ See {Ntswf::Utils}
65
+
66
+ License
67
+ -------
68
+ [LPGLv3](http://www.gnu.org/licenses/lgpl-3.0.html)
@@ -0,0 +1,29 @@
1
+ require_relative 'worker'
2
+
3
+ module Ntswf
4
+ # Interface for a worker executing tasks
5
+ module ActivityWorker
6
+ include Ntswf::Worker
7
+
8
+ def process_activity_task
9
+ announce("polling for activity task #{activity_task_list}")
10
+ domain.activity_tasks.poll_for_single_task(activity_task_list) do |task|
11
+ announce("got activity task #{task.activity_type.inspect} #{task.input}")
12
+ begin
13
+ if task.activity_type == activity_type
14
+ yield task
15
+ else
16
+ raise "unknown activity type: #{task.activity_type.inspect}"
17
+ end
18
+ rescue => e
19
+ notify(e, activity_type: task.activity_type.inspect, input: task.input)
20
+ details = {
21
+ error: e.message[0, 1000],
22
+ exception: e.class.to_s[0, 1000],
23
+ }
24
+ task.fail!(details: details.to_json, reason: 'Exception')
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/ntswf/base.rb ADDED
@@ -0,0 +1,93 @@
1
+ require 'aws'
2
+
3
+ module Ntswf
4
+ module Base
5
+ # @param config [Hash] A configuration with the following keys:
6
+ # @option config [String] :access_key_id AWS credential
7
+ # @option config [Hash] :activity_task_lists The task list names for activities as hash (see also *:unit*)
8
+ # @option config [String] :decision_task_list The task list name for decisions
9
+ # @option config [String] :domain The SWF domain name
10
+ # @option config [Numeric] :execution_version Value allowing clients to reject future execution versions
11
+ # @option config [String] :secret_access_key AWS credential
12
+ # @option config [String] :unit This worker/client's activity task list key
13
+ def initialize(config)
14
+ @config = config.with_indifferent_access
15
+ raise_if_invalid_task_list
16
+ end
17
+
18
+ # @return [AWS::SimpleWorkflow]
19
+ def swf
20
+ @swf ||= AWS::SimpleWorkflow.new(@config.slice(:access_key_id, :secret_access_key).merge(
21
+ use_ssl: true))
22
+ end
23
+
24
+ def workflow_name
25
+ "#{default_unit}-workflow"
26
+ end
27
+
28
+ def workflow_version
29
+ "v1"
30
+ end
31
+
32
+ def domain
33
+ @domain ||= swf.domains[@config[:domain]]
34
+ end
35
+
36
+ def activity_task_lists
37
+ @config[:activity_task_lists]
38
+ end
39
+
40
+ def decision_task_list
41
+ @config[:decision_task_list] || "#{default_unit}-decision-task-list"
42
+ end
43
+
44
+ def activity_task_list
45
+ activity_task_lists[default_unit] || "#{default_unit}-activity-task-list"
46
+ end
47
+
48
+ def default_unit
49
+ @default_unit ||= @config[:unit].to_s
50
+ end
51
+
52
+ def execution_version
53
+ @config[:execution_version]
54
+ end
55
+
56
+ # Parse the options stored in a task's *input* value
57
+ # @param input [String] A task's input
58
+ # @return [Hash] Converted input string
59
+ # @see Ntswf::Client#start_execution Hash keys to be expected
60
+ def parse_input(input)
61
+ options, legacy_params = JSON.parse(input)
62
+ options = {name: options} unless options.kind_of? Hash
63
+ options.merge!(params: legacy_params) if legacy_params
64
+ options.with_indifferent_access
65
+ end
66
+
67
+ def notify(message, params)
68
+ log("#{message.message}\n #{message.backtrace.join("\n ")}") if message.kind_of? Exception
69
+ end
70
+
71
+ protected
72
+
73
+ def announce(s)
74
+ $0 = s
75
+ log(s)
76
+ end
77
+
78
+ def log(s)
79
+ $stdout.puts("#{Process.pid} #{s}")
80
+ end
81
+
82
+ def raise_if_invalid_task_list
83
+ for task_list in [*activity_task_lists.values, decision_task_list] do
84
+ if task_list.include?(separator)
85
+ raise "Invalid config '#{task_list}': Separator '#{separator}' is reserved for internal use."
86
+ end
87
+ if task_list.count(". ") > 0
88
+ raise "Invalid config '#{task_list}': Dots and spaces not allowed."
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,45 @@
1
+ require_relative 'base'
2
+
3
+ module Ntswf
4
+ # Interface for an application that wishes to start a task
5
+ module Client
6
+ include Ntswf::Base
7
+
8
+ # Enqueue a new SWF task.
9
+ #
10
+ # The options configure the control flow of the task.
11
+ # Excluding *:execution_id* they will be stored in the *input* argument of the task as JSON.
12
+ # @param options [Hash] The task's options. Keys with special meaning:
13
+ # @option options [String] :execution_id Mandatory workflow ID suffix, allowed IDs are documented at docs.amazonwebservices.com (WorkflowId Property)
14
+ # @option options [Numeric] :interval Optional, in seconds. Enforces periodic re-run of the task, even in case of failure
15
+ # @option options [String] :name Identifies the kind of task for the executing unit
16
+ # @option options [Hash] :params Custom task parameters passed on to the executing unit
17
+ # @option options [String] :unit The executing unit's key, a corresponding activity task list must be configured
18
+ # @option options [Numeric] :version Optional minimum version of the client. The task may be rescheduled by older clients.
19
+ # @return [AWS::SimpleWorkflow::WorkflowExecution]
20
+ # @raise [AWS::SimpleWorkflow::Errors::WorkflowExecutionAlreadyStartedFault]
21
+ def start_execution(options)
22
+ execution_id = options.delete(:execution_id)
23
+ workflow_type.start_execution(
24
+ child_policy: :terminate,
25
+ execution_start_to_close_timeout: 48 * 3600,
26
+ input: options.to_json,
27
+ tag_list: [options[:unit].to_s, options[:name].to_s],
28
+ task_list: decision_task_list,
29
+ task_start_to_close_timeout: 10 * 60,
30
+ workflow_id: [activity_task_list, execution_id].join(separator),
31
+ )
32
+ end
33
+
34
+ # @return [String] separator part of the final *workflow_id*
35
+ def separator
36
+ ";"
37
+ end
38
+
39
+ protected
40
+
41
+ def workflow_type
42
+ @workflow_type ||= domain.workflow_types[workflow_name, workflow_version]
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,105 @@
1
+ require_relative 'worker'
2
+
3
+ module Ntswf
4
+ # Interface for a worker arbitrating tasks, optionally for multiple apps
5
+ module DecisionWorker
6
+ include Ntswf::Worker
7
+
8
+ # Process a decision task.
9
+ # The following task values are interpreted:
10
+ # input:: see {Ntswf::Client#start_execution}
11
+ # reason:: reschedule if {RETRY}
12
+ # result:: Interpreted as {Hash}, see below for keys
13
+ # Result keys
14
+ # :seconds_until_retry:: Planned reschedule after task completion
15
+ def process_decision_task
16
+ announce("polling for decision task #{decision_task_list}")
17
+ domain.decision_tasks.poll_for_single_task(decision_task_list) do |task|
18
+ announce("got decision task #{task.workflow_execution.inspect}")
19
+ begin
20
+ task.new_events.each do |event|
21
+ log("processing event #{event.inspect}")
22
+ case event.event_type
23
+ when 'WorkflowExecutionStarted'
24
+ schedule(task, event)
25
+ when 'TimerFired'
26
+ start_event = task.events.first
27
+ keys = [
28
+ :child_policy,
29
+ :execution_start_to_close_timeout,
30
+ :input,
31
+ :tag_list,
32
+ :task_list,
33
+ :task_start_to_close_timeout,
34
+ ]
35
+ attributes = start_event.attributes.to_h.keep_if {|k| keys.include? k}
36
+ task.continue_as_new_workflow_execution(attributes)
37
+ when 'ActivityTaskCompleted'
38
+ result = parse_result(event.attributes.result)
39
+ start_timer(task, result[:seconds_until_retry]) or task.complete_workflow_execution(
40
+ result: event.attributes.result)
41
+ when 'ActivityTaskFailed'
42
+ if (event.attributes.reason == RETRY)
43
+ schedule(task, task.events.first)
44
+ else
45
+ start_timer(task) or task.fail_workflow_execution(
46
+ event.attributes.to_h.slice(:details, :reason))
47
+ end
48
+ when 'ActivityTaskTimedOut'
49
+ notify("Timeout in Simple Workflow. Possible cause: all workers busy",
50
+ workflow_execution: task.workflow_execution.inspect)
51
+ start_timer(task) or task.cancel_workflow_execution(
52
+ details: 'activity task timeout')
53
+ end
54
+ end
55
+ rescue => e
56
+ notify(e, workflow_execution: task.workflow_execution.inspect)
57
+ raise e
58
+ end
59
+ end
60
+ end
61
+
62
+ protected
63
+
64
+ def start_timer(task, interval = nil)
65
+ unless interval
66
+ options = parse_input(task.events.first.attributes.input)
67
+ interval = options['interval']
68
+ end
69
+ task.start_timer(interval.to_i) if interval
70
+ interval
71
+ end
72
+
73
+ def schedule(task, data_providing_event)
74
+ input = data_providing_event.attributes.input
75
+ options = parse_input(input)
76
+ app_in_charge = options['unit'] || guess_app_from(data_providing_event)
77
+ task_list = activity_task_lists[app_in_charge]
78
+ raise "Missing activity task list config for #{app_in_charge.inspect}" unless task_list
79
+
80
+ task.schedule_activity_task(activity_type, {
81
+ heartbeat_timeout: :none,
82
+ input: input,
83
+ task_list: task_list,
84
+ schedule_to_close_timeout: 12 * 3600,
85
+ schedule_to_start_timeout: 10 * 60,
86
+ start_to_close_timeout: 12 * 3600,
87
+ })
88
+ end
89
+
90
+ def parse_result(result)
91
+ if result
92
+ value = JSON.parse(result) rescue nil # expecting JSON::ParserError
93
+ end
94
+ value = {} unless value.kind_of? Hash
95
+ value.with_indifferent_access
96
+ end
97
+
98
+ private
99
+
100
+ # transitional, until all apps speak the input options protocol
101
+ def guess_app_from(data_providing_event)
102
+ data_providing_event.workflow_execution.workflow_type.name[/\w+/]
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'base'
2
+
3
+ module Ntswf
4
+ module Utils
5
+ include Ntswf::Base
6
+
7
+ def create_domain(description)
8
+ swf.domains.create(@config[:domain], 3, description: description)
9
+ end
10
+
11
+ def activity_name
12
+ "#{default_unit}-activity"
13
+ end
14
+
15
+ def register_workflow_type
16
+ domain.workflow_types.register(workflow_name, workflow_version)
17
+ end
18
+
19
+ def register_activity_type
20
+ domain.activity_types.register(activity_name, workflow_version)
21
+ end
22
+
23
+ def activity_type
24
+ @activity_type ||= domain.activity_types[activity_name, workflow_version]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'base'
2
+
3
+ module Ntswf
4
+ module Worker
5
+ include Ntswf::Base
6
+
7
+ # *reason* value to force task reschedule, may be set if the worker is unable process the task
8
+ RETRY = "Retry"
9
+ end
10
+ end
data/lib/ntswf.rb ADDED
@@ -0,0 +1,14 @@
1
+ module Ntswf
2
+ AUTOLOAD = %w(
3
+ ActivityWorker
4
+ Client
5
+ DecisionWorker
6
+ Utils
7
+ )
8
+
9
+ AUTOLOAD.each { |c| autoload c.to_sym, "ntswf/#{c.gsub(/.(?=[A-Z])/, '\0_').downcase}.rb" }
10
+
11
+ def self.included(base)
12
+ AUTOLOAD.each { |c| include const_get c }
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ntswf
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Infopark AG
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-01-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aws-sdk
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.8'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.8'
30
+ description: A layer around AWS SWF
31
+ email: info@infopark.de
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - README.md
37
+ - LICENSE
38
+ - lib/ntswf.rb
39
+ - lib/ntswf/base.rb
40
+ - lib/ntswf/utils.rb
41
+ - lib/ntswf/client.rb
42
+ - lib/ntswf/worker.rb
43
+ - lib/ntswf/activity_worker.rb
44
+ - lib/ntswf/decision_worker.rb
45
+ homepage: https://github.com/infopark/ntswf
46
+ licenses:
47
+ - LGPLv3
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: 1.9.3
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 1.8.23
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: Not That Simple Workflow
70
+ test_files: []
71
+ has_rdoc: