cloudmunda 0.1.0 → 0.1.3

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.
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'supervisor'
4
+
5
+ module Cloudmunda
6
+ class Launcher
7
+ attr_reader :supervisor
8
+
9
+ def initialize
10
+ @supervisor = ::Cloudmunda::Supervisor.new
11
+ end
12
+
13
+ # Starts the supervisor and job processors.
14
+ def start
15
+ supervisor.start
16
+ end
17
+
18
+ # Tells the supervisor to stop processing any more jobs.
19
+ def quiet
20
+ supervisor.quiet
21
+ end
22
+
23
+ # Tells the supervisor to stop job processors. This method blocks until
24
+ # all processors are complete and stopped. It can take up to configurable
25
+ # timeout.
26
+ def stop
27
+ supervisor.stop
28
+ end
29
+
30
+ private
31
+
32
+ def client
33
+ ::Cloudmunda.client
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudmunda
4
+ class Processor
5
+ attr_reader :client, :worker_class, :busy_count, :timer
6
+
7
+ def initialize(worker_class:, client: ::Cloudmunda.client)
8
+ @client = client
9
+ @worker_class = worker_class
10
+ @busy_count = ::Concurrent::AtomicFixnum.new(0)
11
+ @timer = ::Concurrent::TimerTask.new(
12
+ run_now: true,
13
+ execution_interval: worker_poll_interval,
14
+ timeout_interval: worker_timeout
15
+ ) { run }
16
+ end
17
+
18
+ def start
19
+ timer.execute
20
+ self
21
+ end
22
+
23
+ def stop
24
+ timer.shutdown
25
+ self
26
+ end
27
+
28
+ def should_activate_jobs?
29
+ busy_count.value <= worker_max_jobs_to_activate
30
+ end
31
+
32
+ private
33
+
34
+ def run
35
+ fetch if should_activate_jobs?
36
+ end
37
+
38
+ def fetch
39
+ activate_jobs_request.each do |response|
40
+ busy_count.increment(response.jobs.count)
41
+ response.jobs.each do |job|
42
+ ::Concurrent::Future.execute do
43
+ process(job)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def process(job)
50
+ worker = worker_class.new(client)
51
+ begin
52
+ logger.info "class=#{worker_class} jid=#{job.key} Start processing #{job.type}"
53
+
54
+ worker.process(job)
55
+ worker.complete_job(job)
56
+
57
+ logger.info "class=#{worker_class} jid=#{job.key} Done processing #{job.type}"
58
+ rescue StandardError => e
59
+ logger.info "class=#{worker_class} jid=#{job.key} Failed processing #{job.type}: #{e.message}"
60
+
61
+ worker.fail_job(job, reason: e.message)
62
+ raise e
63
+ ensure
64
+ busy_count.decrement
65
+ end
66
+ end
67
+
68
+ def activate_jobs_request
69
+ client.activate_jobs(
70
+ type: worker_type,
71
+ worker: worker_name,
72
+ timeout: worker_timeout * 1000,
73
+ maxJobsToActivate: max_jobs_to_activate,
74
+ fetchVariable: worker_variables_to_fetch
75
+ )
76
+ end
77
+
78
+ def worker_type
79
+ worker_class.get_type
80
+ end
81
+
82
+ def worker_name
83
+ worker_class.get_name
84
+ end
85
+
86
+ def worker_max_jobs_to_activate
87
+ worker_class.get_max_jobs_to_activate
88
+ end
89
+
90
+ def worker_timeout
91
+ worker_class.get_timeout
92
+ end
93
+
94
+ def worker_variables_to_fetch
95
+ worker_class.get_variables_to_fetch
96
+ end
97
+
98
+ def worker_poll_interval
99
+ worker_class.get_poll_interval
100
+ end
101
+
102
+ def max_jobs_to_activate
103
+ worker_max_jobs_to_activate - busy_count.value
104
+ end
105
+
106
+ def logger
107
+ ::Cloudmunda.logger
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'processor'
4
+
5
+ module Cloudmunda
6
+ class Supervisor
7
+ def initialize
8
+ @processors = []
9
+ end
10
+
11
+ def start
12
+ workers.each do |worker_class|
13
+ if ::Cloudmunda.env == 'development' && !worker_class.get_runs_in_development
14
+ logger.info "Not starting a processor for worker #{worker_class.get_type} as it doesn't run in development."
15
+ next
16
+ end
17
+
18
+ logger.info "Starting a processor for worker #{worker_class.get_type}"
19
+ processor = ::Cloudmunda::Processor.new(worker_class: worker_class)
20
+ @processors << processor.start
21
+ end
22
+ end
23
+
24
+ def quiet
25
+ logger.info 'Terminating workers'
26
+ @processors.each(&:stop)
27
+ end
28
+
29
+ def stop(timeout: ::Cloudmunda.timeout)
30
+ quiet
31
+ logger.info "Pausing #{timeout}s to allow workers to finish..."
32
+ sleep timeout
33
+ end
34
+
35
+ private
36
+
37
+ def workers
38
+ ::Cloudmunda.workers.to_a
39
+ end
40
+
41
+ def logger
42
+ ::Cloudmunda.logger
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Cloudmunda
6
+ module Worker
7
+ attr_accessor :client, :type, :max_jobs_to_activate, :poll_interval, :timeout, :variables, :runs_in_development
8
+
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ Cloudmunda.register_worker(base)
12
+ end
13
+
14
+ def initialize(client)
15
+ @client = client
16
+ end
17
+
18
+ def complete_job(job, variables: {})
19
+ logger.info "Completed processing job #{job.type} #{job.key}"
20
+ client.complete_job(
21
+ jobKey: job.key,
22
+ variables: Hash(variables).to_json
23
+ )
24
+ end
25
+
26
+ def fail_job(job, reason: '')
27
+ logger.error "Failed processing job #{job.type} #{job.key}: #{reason}"
28
+ client.fail_job(
29
+ jobKey: job.key,
30
+ retries: job.retries - 1,
31
+ errorMessage: reason
32
+ )
33
+ rescue StandardError => e
34
+ logger.error e.message
35
+ end
36
+
37
+ def logger
38
+ ::Cloudmunda.logger
39
+ end
40
+
41
+ module ClassMethods
42
+ # Sets the type of service task the worker should subscribe to.
43
+ #
44
+ # @example
45
+ # class MyWorker
46
+ # include ::Cloudmunda::Worker
47
+ # type "some-service-task-type"
48
+ # end
49
+ #
50
+ # @param [String] type
51
+ # @return [String]
52
+ def type(type)
53
+ @type = type
54
+ end
55
+
56
+ # Returns the type of service task the worker should subscribe to.
57
+ #
58
+ # @return [String]
59
+ def get_type
60
+ @type
61
+ end
62
+
63
+ # Sets the maximum number of jobs to send to the worker for processing at once.
64
+ # As jobs get completed by the worker, more jobs will be sent to the worker
65
+ # but always within this limit.
66
+ #
67
+ # @example
68
+ # class MyWorker
69
+ # include ::Cloudmunda::Worker
70
+ # max_jobs_to_activate 5
71
+ # end
72
+ #
73
+ # @param [Integer] max_jobs_to_activate
74
+ # @return [Integer]
75
+ def max_jobs_to_activate(max_jobs_to_activate)
76
+ @max_jobs_to_activate = max_jobs_to_activate
77
+ end
78
+
79
+ # Returns the maximum number of jobs to send to the worker for processing at once.
80
+ # As jobs get completed by the worker, more jobs will be sent to the worker
81
+ # but always within this limit.
82
+ #
83
+ # @return [Integer]
84
+ def get_max_jobs_to_activate
85
+ @max_jobs_to_activate || 1
86
+ end
87
+
88
+ # Sets the interval duration in seconds between polls to the broker.
89
+ #
90
+ # @example
91
+ # class MyWorker
92
+ # include ::Cloudmunda::Worker
93
+ # poll_interval 5
94
+ # end
95
+ #
96
+ # @param [Integer] poll_interval
97
+ # @return [Integer]
98
+ def poll_interval(poll_interval)
99
+ @poll_interval = poll_interval
100
+ end
101
+
102
+ # Returns the interval duration in seconds between polls to the broker.
103
+ #
104
+ # @return [Integer]
105
+ def get_poll_interval
106
+ @poll_interval || 5
107
+ end
108
+
109
+ # Sets the time in seconds the worker has to process the job before
110
+ # the broker consider it as expired and can schedule it to another worker.
111
+ #
112
+ # @example
113
+ # class MyWorker
114
+ # include ::Cloudmunda::Worker
115
+ # timeout 30
116
+ # end
117
+ #
118
+ # @param [Integer] timeout
119
+ # @return [Integer]
120
+ def timeout(timeout)
121
+ @timeout = timeout
122
+ end
123
+
124
+ # Returns the time in seconds the worker has to process the job before
125
+ # the broker consider it as expired and can schedule it to another worker.
126
+ #
127
+ # @return [Integer]
128
+ def get_timeout
129
+ @timeout || 30
130
+ end
131
+
132
+ # Sets the worker's variables to fetch from the broker when polling for new
133
+ # jobs.
134
+ #
135
+ # @example
136
+ # class MyWorker
137
+ # include ::Cloudmunda::Worker
138
+ # variables [:foo, :bar]
139
+ # end
140
+ #
141
+ # @param [Array<String, Symbol>] variables
142
+ # @return [Array<String, Symbol>]
143
+ def variables(variables)
144
+ @variables = variables
145
+ end
146
+
147
+ # Returns the worker's variables to fetch from the broker when polling for new
148
+ # jobs.
149
+ #
150
+ # @return [Array<String, Symbol>]
151
+ def get_variables_to_fetch
152
+ @variables.to_a
153
+ end
154
+
155
+ # Sets if this service task runs in development mode or not.
156
+ #
157
+ # @example
158
+ # class MyWorker
159
+ # include ::Cloudmunda::Worker
160
+ # runs_in_development true
161
+ # end
162
+ #
163
+ # @param [Boolean] runs_in_development
164
+ # @return [Boolean]
165
+ def runs_in_development(runs_in_development)
166
+ @runs_in_development = runs_in_development
167
+ end
168
+
169
+ # Returns if this service task should run in development mode.
170
+ #
171
+ # @return [Boolean]
172
+ def get_runs_in_development
173
+ @runs_in_development || false
174
+ end
175
+
176
+ # Returns the worker's name.
177
+ #
178
+ # @return [String]
179
+ def get_name
180
+ name = self.name.gsub(/::/, ':')
181
+ name.gsub!(/([^A-Z:])([A-Z])/) { "#{Regexp.last_match(1)}_#{Regexp.last_match(2)}" }
182
+ name.downcase
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'optparse'
5
+ require 'fileutils'
6
+ require 'cloudmunda'
7
+ require 'cloudmunda/cli/launcher'
8
+
9
+ $stdout.sync = true
10
+
11
+ module Cloudmunda
12
+ class CLI
13
+ include Singleton
14
+
15
+ attr_accessor :launcher
16
+
17
+ def parse(argv = ARGV)
18
+ parse_options(argv)
19
+ end
20
+
21
+ def run
22
+ boot
23
+
24
+ self_read, self_write = IO.pipe
25
+ sigs = %w[INT TERM]
26
+ sigs.each do |sig|
27
+ trap sig do
28
+ self_write.write("#{sig}\n")
29
+ end
30
+ rescue ArgumentError
31
+ logger.warn "Signal #{sig} not supported"
32
+ end
33
+
34
+ launch(self_read)
35
+ end
36
+
37
+ private
38
+
39
+ def parse_options(argv)
40
+ option_parser.parse!(argv)
41
+ end
42
+
43
+ def option_parser
44
+ OptionParser.new.tap do |p|
45
+ p.on '-e', '--env ENV', 'Application environment' do |arg|
46
+ config.env = arg
47
+ end
48
+
49
+ p.on '-i', '--client-id CLIENT_ID', 'Client ID' do |arg|
50
+ config.client_id = arg
51
+ end
52
+
53
+ p.on '-r', '--require [PATH|DIR]', 'Location of Rails application with workers or file to require' do |arg|
54
+ if !File.exist?(arg) ||
55
+ (File.directory?(arg) && !File.exist?("#{arg}/config/application.rb"))
56
+ raise ArgumentError, "#{arg} is not a ruby file nor a rails application"
57
+ else
58
+ config.require = arg
59
+ end
60
+ end
61
+
62
+ p.on '-s', '--client-secret CLIENT_SECRET', 'Client Secret' do |arg|
63
+ config.client_secret = arg
64
+ end
65
+
66
+ p.on '-u', '--zeebe-url ZEEBE_URL', 'Zeebe URL' do |arg|
67
+ config.zeebe_url = arg
68
+ end
69
+
70
+ p.on '-U', '--zeebe-auth-url ZEEBE_AUTH_URL', 'Zeebe Authorization Server URL' do |arg|
71
+ config.auth_url = arg
72
+ end
73
+
74
+ p.on '-a', '--audience URL', 'Zeebe Audience' do |arg|
75
+ config.audience = arg
76
+ end
77
+
78
+ p.on '-T', '--use-access-token STRING', 'Zeebe Audience' do |arg|
79
+ config.use_access_token = arg.to_s.downcase == 'true'
80
+ end
81
+
82
+ p.on '-t', '--timeout NUM', 'Shutdown timeout' do |arg|
83
+ timeout = Integer(arg)
84
+ raise ArgumentError, 'timeout must be a positive integer' if timeout <= 0
85
+
86
+ config.timeout = timeout
87
+ end
88
+
89
+ # p.on "-v", "--verbose", "Print more verbose output" do |arg|
90
+ # ::Cloudmuna.logger.level = ::Logger::DEBUG
91
+ # end
92
+
93
+ p.on '-V', '--version', 'Print version and exit' do |_arg|
94
+ puts "Cloudmunda #{::Cloudmunda::VERSION}"
95
+ exit(0)
96
+ end
97
+
98
+ p.banner = 'Usage: cloudmunda [options]'
99
+ p.on_tail '-h', '--help', 'Show help' do
100
+ puts p
101
+
102
+ exit(1)
103
+ end
104
+ end
105
+ end
106
+
107
+ def boot
108
+ ENV['RACK_ENV'] = ENV['RAILS_ENV'] = config.env
109
+
110
+ if File.directory?(config.require)
111
+ require 'rails'
112
+ raise 'Cloudmunda does not supports this version of Rails' if ::Rails::VERSION::MAJOR < 6
113
+
114
+ require File.expand_path("#{config.require}/config/environment.rb")
115
+ Dir[Rails.root.join('app/jobs/**/*.rb')].sort.each { |f| require f }
116
+
117
+ logger.info "Booted Rails #{::Rails.version} application in #{config.env} environment"
118
+ else
119
+ require config.require
120
+ end
121
+ end
122
+
123
+ def launch(self_read)
124
+ @launcher = ::Cloudmunda::Launcher.new
125
+
126
+ logger.info 'Starting processing, hit Ctrl-C to stop' if config.env == 'development' && $stdout.tty?
127
+
128
+ begin
129
+ launcher.start
130
+
131
+ while (readable_io = IO.select([self_read]))
132
+ signal = readable_io.first[0].gets.strip
133
+ handle_signal(signal)
134
+ end
135
+ rescue Interrupt
136
+ logger.info 'Shutting down'
137
+ launcher.stop
138
+ logger.info 'Bye!'
139
+
140
+ exit(0)
141
+ end
142
+ end
143
+
144
+ def handle_signal(signal)
145
+ handler = signal_handlers[signal]
146
+ if handler
147
+ handler.call(self)
148
+ else
149
+ logger.warn "No signal handler for #{signal}"
150
+ end
151
+ end
152
+
153
+ def signal_handlers
154
+ {
155
+ 'INT' => ->(_cli) { raise Interrupt },
156
+ 'TERM' => ->(_cli) { raise Interrupt }
157
+ }
158
+ end
159
+
160
+ def config
161
+ ::Cloudmunda
162
+ end
163
+
164
+ def logger
165
+ ::Cloudmunda.logger
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudmunda
4
+ module Configuration
5
+ VALID_OPTIONS_KEYS = %i[env require logger timeout zeebe_url auth_url client_id client_secret audience
6
+ graphql_url].freeze
7
+ attr_accessor(*VALID_OPTIONS_KEYS)
8
+
9
+ # Sets all configuration options to their default values when this module is extended.
10
+ def self.extended(base)
11
+ base.reset
12
+ end
13
+
14
+ def configure
15
+ yield self
16
+ end
17
+
18
+ def config
19
+ VALID_OPTIONS_KEYS.inject({}) do |option, key|
20
+ option.merge!(key => send(key))
21
+ end
22
+ end
23
+
24
+ # Resets all configuration options to the defaults.
25
+ def reset
26
+ @env = ENV['APP_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
27
+ @logger = Logger.new($stdout)
28
+ @require = '.'
29
+ @timeout = @env == 'development' ? 5 : 30
30
+ @zeebe_url = ENV['ZEEBE_URL']
31
+ @auth_url = ENV['ZEEBE_AUTHORIZATION_SERVER_URL']
32
+ @client_id = ENV['ZEEBE_CLIENT_ID']
33
+ @client_secret = ENV['ZEEBE_CLIENT_SECRET']
34
+ @audience = ENV['ZEEBE_AUDIENCE']
35
+ @graphql_url = ENV['GRAPHQL_URL']
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rest-client'
4
+ require 'json'
5
+ require 'uri'
6
+ require 'base64'
7
+
8
+ module Cloudmunda
9
+ module Graphql
10
+ class Client
11
+ def self.post(params = {})
12
+ response = RestClient.post(Cloudmunda.graphql_url, params.to_json, headers.merge(content_headers))
13
+ JSON.parse(response.body)
14
+ end
15
+
16
+ def self.headers
17
+ access_token = Cloudmunda::API::AccessToken.create(audience_url: 'tasklist.camunda.io').access_token
18
+ {
19
+ authorization: "Bearer #{access_token}"
20
+ }
21
+ end
22
+
23
+ def self.content_headers
24
+ {
25
+ 'Content-Type': 'application/json'
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudmunda
4
+ module Graphql
5
+ class UserTasks
6
+ def self.all
7
+ query = "{
8
+ tasks(query: { state: CREATED })
9
+ {
10
+ id
11
+ taskDefinitionId
12
+ name
13
+ taskState
14
+ assignee
15
+ taskState
16
+ isFirst
17
+ formKey
18
+ processDefinitionId
19
+ completionTime
20
+ processName
21
+ variables {
22
+ name
23
+ value
24
+ }
25
+ }
26
+ }"
27
+ Cloudmunda::Graphql::Client.post(query: query)['data']['tasks']
28
+ end
29
+
30
+ def self.run_mutation(mutation)
31
+ Cloudmunda::Graphql::Client.post(query: mutation)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'graphql/client'
4
+ require_relative 'graphql/user_tasks'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudmunda
4
+ module Loggable
5
+ def logger
6
+ @logger || setup_logger
7
+ end
8
+
9
+ def logger=(logger)
10
+ @logger = logger
11
+ end
12
+
13
+ def setup_logger
14
+ @logger = Beez.config.logger
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cloudmunda
4
- VERSION = "0.1.0"
4
+ VERSION = '0.1.3'
5
5
  end