zenaton 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +45 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +20 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +106 -0
- data/LICENSE.txt +21 -0
- data/README.md +115 -0
- data/Rakefile +8 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/lib/zenaton.rb +12 -0
- data/lib/zenaton/client.rb +211 -0
- data/lib/zenaton/engine.rb +72 -0
- data/lib/zenaton/exceptions.rb +24 -0
- data/lib/zenaton/interfaces/event.rb +9 -0
- data/lib/zenaton/interfaces/job.rb +16 -0
- data/lib/zenaton/interfaces/task.rb +16 -0
- data/lib/zenaton/interfaces/workflow.rb +23 -0
- data/lib/zenaton/parallel.rb +24 -0
- data/lib/zenaton/processor.rb +11 -0
- data/lib/zenaton/query/builder.rb +69 -0
- data/lib/zenaton/services/http.rb +80 -0
- data/lib/zenaton/services/properties.rb +142 -0
- data/lib/zenaton/services/serializer.rb +156 -0
- data/lib/zenaton/tasks/wait.rb +46 -0
- data/lib/zenaton/traits/with_duration.rb +49 -0
- data/lib/zenaton/traits/with_timestamp.rb +131 -0
- data/lib/zenaton/traits/zenatonable.rb +36 -0
- data/lib/zenaton/version.rb +6 -0
- data/lib/zenaton/workflows/version.rb +61 -0
- data/zenaton.gemspec +39 -0
- metadata +260 -0
data/bin/setup
ADDED
data/lib/zenaton.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zenaton/version'
|
4
|
+
require 'zenaton/exceptions'
|
5
|
+
require 'zenaton/client'
|
6
|
+
require 'zenaton/interfaces/event'
|
7
|
+
require 'zenaton/tasks/wait'
|
8
|
+
require 'zenaton/parallel'
|
9
|
+
|
10
|
+
# Top level namespace for the Zenaton ruby library
|
11
|
+
module Zenaton
|
12
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'zenaton/services/http'
|
5
|
+
require 'zenaton/services/properties'
|
6
|
+
require 'zenaton/services/serializer'
|
7
|
+
require 'zenaton/workflows/version'
|
8
|
+
|
9
|
+
module Zenaton
|
10
|
+
# Zenaton Client
|
11
|
+
class Client
|
12
|
+
include Singleton
|
13
|
+
|
14
|
+
ZENATON_API_URL = 'https://zenaton.com/api/v1' # Zenaton api url
|
15
|
+
ZENATON_WORKER_URL = 'http://localhost' # Default worker url
|
16
|
+
DEFAULT_WORKER_PORT = 4001 # Default worker port
|
17
|
+
WORKER_API_VERSION = 'v_newton' # Default worker api version
|
18
|
+
|
19
|
+
MAX_ID_SIZE = 256 # Limit on length of custom ids
|
20
|
+
|
21
|
+
APP_ENV = 'app_env' # Parameter name for the application environment
|
22
|
+
APP_ID = 'app_id' # Parameter name for the application ID
|
23
|
+
API_TOKEN = 'api_token' # Parameter name for the API token
|
24
|
+
|
25
|
+
ATTR_ID = 'custom_id' # Parameter name for custom ids
|
26
|
+
ATTR_NAME = 'name' # Parameter name for workflow names
|
27
|
+
ATTR_CANONICAL = 'canonical_name' # Parameter name for version name
|
28
|
+
ATTR_DATA = 'data' # Parameter name for json payload
|
29
|
+
ATTR_PROG = 'programming_language' # Parameter name for the language
|
30
|
+
ATTR_MODE = 'mode' # Parameter name for the worker update mode
|
31
|
+
|
32
|
+
PROG = 'Ruby' # The current programming language
|
33
|
+
|
34
|
+
EVENT_INPUT = 'event_input' # Parameter name for event data
|
35
|
+
EVENT_NAME = 'event_name' # Parameter name for event name
|
36
|
+
|
37
|
+
WORKFLOW_KILL = 'kill' # Worker update mode to stop a worker
|
38
|
+
WORKFLOW_PAUSE = 'pause' # Worker udpate mode to pause a worker
|
39
|
+
WORKFLOW_RUN = 'run' # Worker update mode to resume a worker
|
40
|
+
|
41
|
+
attr_writer :app_id, :api_token, :app_env
|
42
|
+
|
43
|
+
# Class method that sets the three tokens needed to interact with the API
|
44
|
+
# @param app_id [String] the ID of your Zenaton application
|
45
|
+
# @param api_token [String] your Zenaton account API token
|
46
|
+
# @param app_env [String] the environment (dev, staging, prod) to run under
|
47
|
+
# @return [Zenaton::Client] the instance of the client.
|
48
|
+
def self.init(app_id, api_token, app_env)
|
49
|
+
instance.tap do |client|
|
50
|
+
client.app_id = app_id
|
51
|
+
client.api_token = api_token
|
52
|
+
client.app_env = app_env
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @private
|
57
|
+
def initialize
|
58
|
+
@http = Services::Http.new
|
59
|
+
@serializer = Services::Serializer.new
|
60
|
+
@properties = Services::Properties.new
|
61
|
+
end
|
62
|
+
|
63
|
+
# Gets the url for the workers
|
64
|
+
# @param resource [String] the endpoint for the worker
|
65
|
+
# @param params [String] url encoded parameters to include in request
|
66
|
+
# @return [String] the workers url with parameters
|
67
|
+
def worker_url(resource = '', params = '')
|
68
|
+
base_url = ENV['ZENATON_WORKER_URL'] || ZENATON_WORKER_URL
|
69
|
+
port = ENV['ZENATON_WORKER_PORT'] || DEFAULT_WORKER_PORT
|
70
|
+
url = "#{base_url}:#{port}/api/#{WORKER_API_VERSION}/#{resource}?"
|
71
|
+
add_app_env(url, params)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Gets the url for zenaton api
|
75
|
+
# @param resource [String] the endpoint for the api
|
76
|
+
# @param params [String] url encoded parameters to include in request
|
77
|
+
# @return [String] the api url with parameters
|
78
|
+
def website_url(resource = '', params = '')
|
79
|
+
api_url = ENV['ZENATON_API_URL'] || ZENATON_API_URL
|
80
|
+
url = "#{api_url}/#{resource}?#{API_TOKEN}=#{@api_token}&"
|
81
|
+
add_app_env(url, params)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Start the specified workflow
|
85
|
+
# @param flow [Zenaton::Interfaces::Workflow]
|
86
|
+
def start_workflow(flow)
|
87
|
+
@http.post(
|
88
|
+
instance_worker_url,
|
89
|
+
ATTR_PROG => PROG,
|
90
|
+
ATTR_CANONICAL => canonical_name(flow),
|
91
|
+
ATTR_NAME => class_name(flow),
|
92
|
+
ATTR_DATA => @serializer.encode(@properties.from(flow)),
|
93
|
+
ATTR_ID => parse_custom_id_from(flow)
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Stops a workflow
|
98
|
+
# @param workflow_name [String] the class name of the workflow
|
99
|
+
# @param custom_id [String] the custom ID of the workflow (if any)
|
100
|
+
# @return [NilClass]
|
101
|
+
def kill_workflow(workflow_name, custom_id)
|
102
|
+
update_instance(workflow_name, custom_id, WORKFLOW_KILL)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Pauses a workflow
|
106
|
+
# @param workflow_name [String] the class name of the workflow
|
107
|
+
# @param custom_id [String] the custom ID of the workflow (if any)
|
108
|
+
# @return [NilClass]
|
109
|
+
def pause_workflow(workflow_name, custom_id)
|
110
|
+
update_instance(workflow_name, custom_id, WORKFLOW_PAUSE)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Resumes a workflow
|
114
|
+
# @param workflow_name [String] the class name of the workflow
|
115
|
+
# @param custom_id [String] the custom ID of the workflow (if any)
|
116
|
+
# @return [NilClass]
|
117
|
+
def resume_workflow(workflow_name, custom_id)
|
118
|
+
update_instance(workflow_name, custom_id, WORKFLOW_RUN)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Finds a workflow
|
122
|
+
# @param workflow_name [String] the class name of the workflow
|
123
|
+
# @param custom_id [String] the custom ID of the workflow (if any)
|
124
|
+
# @return [Zenaton::Interfaces::Workflow]
|
125
|
+
def find_workflow(workflow_name, custom_id)
|
126
|
+
# rubocop:disable Metrics/LineLength
|
127
|
+
params = "#{ATTR_ID}=#{custom_id}&#{ATTR_NAME}=#{workflow_name}&#{ATTR_PROG}=#{PROG}"
|
128
|
+
# rubocop:enable Metrics/LineLength
|
129
|
+
data = @http.get(instance_website_url(params))['data']
|
130
|
+
data && @properties.object_from(
|
131
|
+
data['name'],
|
132
|
+
@serializer.decode(data['properties'])
|
133
|
+
)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Sends an event to a workflow
|
137
|
+
# @param workflow_name [String] the class name of the workflow
|
138
|
+
# @param custom_id [String] the custom ID of the workflow (if any)
|
139
|
+
# @param event [Zenaton::Interfaces::Event] the event to send
|
140
|
+
# @return [NilClass]
|
141
|
+
def send_event(workflow_name, custom_id, event)
|
142
|
+
body = {
|
143
|
+
ATTR_PROG => PROG,
|
144
|
+
ATTR_NAME => workflow_name,
|
145
|
+
ATTR_ID => custom_id,
|
146
|
+
EVENT_NAME => event.class.name,
|
147
|
+
EVENT_INPUT => @serializer.encode(@properties.from(event))
|
148
|
+
}
|
149
|
+
@http.post(send_event_url, body)
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def add_app_env(url, params)
|
155
|
+
app_env = @app_env ? "#{APP_ENV}=#{@app_env}&" : ''
|
156
|
+
app_id = @app_id ? "#{APP_ID}=#{@app_id}&" : ''
|
157
|
+
|
158
|
+
"#{url}#{app_env}#{app_id}#{params}"
|
159
|
+
end
|
160
|
+
|
161
|
+
def instance_website_url(params)
|
162
|
+
website_url('instances', params)
|
163
|
+
end
|
164
|
+
|
165
|
+
def instance_worker_url(params = '')
|
166
|
+
worker_url('instances', params)
|
167
|
+
end
|
168
|
+
|
169
|
+
def send_event_url
|
170
|
+
worker_url('events')
|
171
|
+
end
|
172
|
+
|
173
|
+
# rubocop:disable Metrics/MethodLength
|
174
|
+
def parse_custom_id_from(flow)
|
175
|
+
custom_id = flow.id
|
176
|
+
if custom_id
|
177
|
+
unless custom_id.is_a?(String) || custom_id.is_a?(Integer)
|
178
|
+
raise InvalidArgumentError,
|
179
|
+
'Provided ID must be a string or an integer'
|
180
|
+
end
|
181
|
+
custom_id = custom_id.to_s
|
182
|
+
if custom_id.length > MAX_ID_SIZE
|
183
|
+
raise InvalidArgumentError,
|
184
|
+
"Provided Id must not exceed #{MAX_ID_SIZE} bytes"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
custom_id
|
188
|
+
end
|
189
|
+
# rubocop:enable Metrics/MethodLength
|
190
|
+
|
191
|
+
def canonical_name(flow)
|
192
|
+
flow.class.name if flow.is_a? Workflows::Version
|
193
|
+
end
|
194
|
+
|
195
|
+
def class_name(flow)
|
196
|
+
return flow.class.name unless flow.is_a? Workflows::Version
|
197
|
+
flow.current_implementation.class.name
|
198
|
+
end
|
199
|
+
|
200
|
+
def update_instance(workflow_name, custom_id, mode)
|
201
|
+
params = "#{ATTR_ID}=#{custom_id}"
|
202
|
+
url = instance_worker_url(params)
|
203
|
+
options = {
|
204
|
+
ATTR_PROG => PROG,
|
205
|
+
ATTR_NAME => workflow_name,
|
206
|
+
ATTR_MODE => mode
|
207
|
+
}
|
208
|
+
@http.put(url, options)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'zenaton/exceptions'
|
5
|
+
require 'zenaton/interfaces/task'
|
6
|
+
require 'zenaton/interfaces/workflow'
|
7
|
+
require 'zenaton/client'
|
8
|
+
require 'zenaton/processor'
|
9
|
+
|
10
|
+
module Zenaton
|
11
|
+
# Zenaton Engine is a singleton class that stores a reference to the current
|
12
|
+
# client and processor. It then handles job processing either locally or
|
13
|
+
# through the processor with Zenaton workers
|
14
|
+
# To access the instance, call `Zenaton::Engine.instance`
|
15
|
+
class Engine
|
16
|
+
include Singleton
|
17
|
+
|
18
|
+
# @param value [Zenaton::Processor]
|
19
|
+
attr_writer :processor
|
20
|
+
|
21
|
+
# @private
|
22
|
+
def initialize
|
23
|
+
@client = Zenaton::Client.instance
|
24
|
+
@processor = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# Executes jobs synchronously
|
28
|
+
# @param jobs [Array<Zenaton::Interfaces::Job>]
|
29
|
+
# @return [Array<String>, nil] the results if executed locally, or nil
|
30
|
+
def execute(jobs)
|
31
|
+
jobs.map(&method(:check_argument))
|
32
|
+
return jobs.map(&:handle) if process_locally?(jobs)
|
33
|
+
@processor.process(jobs, true)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Executes jobs asynchronously
|
37
|
+
# @param jobs [Array<Zenaton::Interfaces::Job>]
|
38
|
+
# @return nil
|
39
|
+
def dispatch(jobs)
|
40
|
+
jobs.map(&method(:check_argument))
|
41
|
+
jobs.map(&method(:local_dispatch)) if process_locally?(jobs)
|
42
|
+
@processor&.process(jobs, false) unless jobs.length.zero?
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def process_locally?(jobs)
|
49
|
+
jobs.length.zero? || @processor.nil?
|
50
|
+
end
|
51
|
+
|
52
|
+
def local_dispatch(job)
|
53
|
+
if job.is_a? Interfaces::Workflow
|
54
|
+
@client.start_workflow(job)
|
55
|
+
else
|
56
|
+
job.handle
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def check_argument(job)
|
61
|
+
raise InvalidArgumentError, error_message unless valid_job?(job)
|
62
|
+
end
|
63
|
+
|
64
|
+
def error_message
|
65
|
+
'You can only execute or dispatch Zenaton Task or Worflow'
|
66
|
+
end
|
67
|
+
|
68
|
+
def valid_job?(job)
|
69
|
+
job.is_a?(Interfaces::Task) || job.is_a?(Interfaces::Workflow)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zenaton
|
4
|
+
# Zenaton base error class
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Exception raised when communication with workers failed
|
8
|
+
class InternalError < Error; end
|
9
|
+
|
10
|
+
# Exception raise when clien code is invalid
|
11
|
+
class ExternalError < Error; end
|
12
|
+
|
13
|
+
# Exception raised when wrong argument type is provided
|
14
|
+
class InvalidArgumentError < ExternalError; end
|
15
|
+
|
16
|
+
# :nodoc:
|
17
|
+
class UnknownWorkflowError < ExternalError; end
|
18
|
+
|
19
|
+
# Exception raised when network connectivity is lost
|
20
|
+
class ConnectionError < Error; end
|
21
|
+
|
22
|
+
# Exception raised when interfaces are not fulfilled by client code
|
23
|
+
class NotImplemented < Error; end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zenaton/exceptions'
|
4
|
+
|
5
|
+
module Zenaton
|
6
|
+
# Abstract classes used as interfaces
|
7
|
+
module Interfaces
|
8
|
+
# @abstract Do not subclass job directly, use either Tasks or Workflows
|
9
|
+
class Job
|
10
|
+
# Child classes should implement the handle method
|
11
|
+
def handle
|
12
|
+
raise NotImplemented, "Your job does not implement the `handle' method"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zenaton/interfaces/job'
|
4
|
+
|
5
|
+
module Zenaton
|
6
|
+
module Interfaces
|
7
|
+
# @abstract Subclass and override {#handle} to define your custom tasks
|
8
|
+
class Task < Job
|
9
|
+
# Child classes should implement the handle method
|
10
|
+
def handle
|
11
|
+
raise NotImplemented,
|
12
|
+
"Your workflow does not implement the `handle' method"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zenaton/interfaces/job'
|
4
|
+
|
5
|
+
module Zenaton
|
6
|
+
module Interfaces
|
7
|
+
# @abstract Subclass and override {#handle} to implement a custom Workflow
|
8
|
+
class Workflow < Job
|
9
|
+
# Method called to run the workflow
|
10
|
+
def handle
|
11
|
+
raise NotImplemented,
|
12
|
+
"Your workflow does not implement the `handle' method"
|
13
|
+
end
|
14
|
+
|
15
|
+
# (Optional) Implement this method if you want to use custom IDs for your
|
16
|
+
# workflows.
|
17
|
+
# @return [String, Integer, NilClass] the custom id. Must be <= 256 bytes.
|
18
|
+
def id
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zenaton/engine'
|
4
|
+
|
5
|
+
module Zenaton
|
6
|
+
# Convenience class to execute jobs in parallel
|
7
|
+
class Parallel
|
8
|
+
# Build a collection of jobs to be executed in parallel
|
9
|
+
# @param items [Zenaton::Interfaces::Job]
|
10
|
+
def initialize(*items)
|
11
|
+
@items = items
|
12
|
+
end
|
13
|
+
|
14
|
+
# Execute synchronous jobs
|
15
|
+
def execute
|
16
|
+
Engine.instance.execute(@items)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Dispatches asynchronous jobs
|
20
|
+
def dispatch
|
21
|
+
Engine.instance.dispatch(@items)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zenaton/exceptions'
|
4
|
+
require 'zenaton/client'
|
5
|
+
require 'zenaton/interfaces/workflow'
|
6
|
+
|
7
|
+
module Zenaton
|
8
|
+
# Wrapper module for interacting with jobs
|
9
|
+
module Query
|
10
|
+
# Wrapper class around the client to interact with workflows by id
|
11
|
+
class Builder
|
12
|
+
def initialize(klass)
|
13
|
+
check_klass(klass)
|
14
|
+
@klass = klass
|
15
|
+
@client = Client.instance
|
16
|
+
end
|
17
|
+
|
18
|
+
# Sets the id of the workflow we want to find
|
19
|
+
# @param id [String, NilClass] the id
|
20
|
+
# @return [Zenaton::Query::Builder] the current builder
|
21
|
+
def where_id(id)
|
22
|
+
@id = id
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# Finds a workflow
|
27
|
+
# @return [Zenaton::Interfaces::Workflow]
|
28
|
+
def find
|
29
|
+
@client.find_workflow(@klass, @id)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Sends an event to a workflow
|
33
|
+
# @param event [Zenaton::Interfaces::Event] the event to send
|
34
|
+
# @return [Zenaton::Query::Builder] the current builder
|
35
|
+
def send_event(event)
|
36
|
+
@client.send_event(@klass, @id, event)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# Stops a workflow
|
41
|
+
# @return [Zenaton::Query::Builder] the current builder
|
42
|
+
def kill
|
43
|
+
@client.kill_workflow(@klass, @id)
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
# Pauses a workflow
|
48
|
+
# @return [Zenaton::Query::Builder] the current builder
|
49
|
+
def pause
|
50
|
+
@client.pause_workflow(@klass, @id)
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
# Resumes a workflow
|
55
|
+
# @return [Zenaton::Query::Builder] the current builder
|
56
|
+
def resume
|
57
|
+
@client.resume_workflow(@klass, @id)
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def check_klass(klass)
|
64
|
+
msg = "#{klass} should be a subclass of Zenaton::Interfaces::Workflow"
|
65
|
+
raise ExternalError, msg unless klass < Interfaces::Workflow
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|