zenaton 0.1.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.
- 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
|