legionio 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +98 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +38 -8
  5. data/CHANGELOG.md +7 -0
  6. data/Gemfile +12 -9
  7. data/LICENSE.txt +21 -0
  8. data/README.md +46 -0
  9. data/Rakefile +1 -1
  10. data/bin/console +3 -2
  11. data/bin/legion +9 -6
  12. data/bin/test +28 -1
  13. data/bitbucket-pipelines.yml +13 -8
  14. data/legion.gemspec +27 -21
  15. data/lib/legion.rb +14 -5
  16. data/lib/legion/exceptions/handled_task.rb +6 -0
  17. data/lib/legion/exceptions/missingargument.rb +2 -2
  18. data/lib/legion/extensions.rb +151 -0
  19. data/lib/legion/extensions/actors/base.rb +53 -0
  20. data/lib/legion/extensions/actors/every.rb +50 -0
  21. data/lib/legion/extensions/actors/loop.rb +34 -0
  22. data/lib/legion/extensions/actors/nothing.rb +15 -0
  23. data/lib/legion/extensions/actors/once.rb +42 -0
  24. data/lib/legion/extensions/actors/poll.rb +90 -0
  25. data/lib/legion/extensions/actors/subscription.rb +120 -0
  26. data/lib/legion/extensions/builders/actors.rb +62 -0
  27. data/lib/legion/extensions/builders/base.rb +38 -0
  28. data/lib/legion/extensions/builders/helpers.rb +26 -0
  29. data/lib/legion/extensions/builders/runners.rb +54 -0
  30. data/lib/legion/extensions/core.rb +84 -0
  31. data/lib/legion/extensions/helpers/base.rb +88 -0
  32. data/lib/legion/extensions/helpers/core.rb +20 -0
  33. data/lib/legion/extensions/helpers/lex.rb +22 -0
  34. data/lib/legion/extensions/helpers/logger.rb +48 -0
  35. data/lib/legion/extensions/helpers/task.rb +42 -0
  36. data/lib/legion/extensions/helpers/transport.rb +45 -0
  37. data/lib/legion/extensions/transport.rb +156 -0
  38. data/lib/legion/process.rb +8 -3
  39. data/lib/legion/runner.rb +57 -0
  40. data/lib/legion/runner/log.rb +12 -0
  41. data/lib/legion/runner/status.rb +72 -0
  42. data/lib/legion/service.rb +44 -27
  43. data/lib/legion/supervison.rb +10 -24
  44. data/lib/legion/version.rb +1 -1
  45. metadata +184 -46
  46. data/lib/legion/extension/loader.rb +0 -96
  47. data/lib/legion/runners/runner.rb +0 -58
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ module Legion
5
+ module Extensions
6
+ module Helpers
7
+ module Core
8
+ include Legion::Extensions::Helpers::Base
9
+
10
+ def settings
11
+ if Legion::Settings[:extensions].key?(lex_filename.to_sym)
12
+ Legion::Settings[:extensions][lex_filename.to_sym]
13
+ else
14
+ { logger: { level: 'info', extended: false } }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Helpers
6
+ module Lex
7
+ include Legion::Extensions::Helpers::Core
8
+ include Legion::Extensions::Helpers::Logger
9
+
10
+ def self.included(base)
11
+ base.send :extend, Legion::Extensions::Helpers::Core if base.class == Class
12
+ base.send :extend, Legion::Extensions::Helpers::Logger if base.class == Class
13
+ base.extend base if base.class == Module
14
+ end
15
+
16
+ def default_settings
17
+ { logger: { level: 'info' } }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Helpers
6
+ module Logger
7
+ def log
8
+ return @log unless @log.nil?
9
+
10
+ logger_hash = { lex: lex_filename || nil }
11
+ logger_hash[:lex] = lex_filename.first if logger_hash[:lex].is_a? Array
12
+ if respond_to?(:settings) && settings.key?(:logger)
13
+ logger_hash[:level] = settings[:logger][:level] if settings[:logger].key? :level
14
+ logger_hash[:level] = settings[:logger][:log_file] if settings[:logger].key? :log_file
15
+ logger_hash[:trace] = settings[:logger][:trace] if settings[:logger].key? :trace
16
+ logger_hash[:extended] = settings[:logger][:extended] if settings[:logger].key? :extended
17
+ elsif respond_to?(:settings)
18
+ Legion::Logging.warn Legion::Settings[:extensions][lex_filename.to_sym]
19
+ Legion::Logging.warn "#{lex_name} has settings but no :logger key"
20
+ else
21
+ Legion::Logging.warn 'no settings'
22
+ end
23
+ @log = Legion::Logging::Logger.new(logger_hash)
24
+ end
25
+
26
+ def handle_exception(exception, task_id: nil, **opts)
27
+ log.error exception.message + " for task_id: #{task_id} but was logged "
28
+ log.error exception.backtrace[0..10]
29
+ log.error opts
30
+
31
+ unless task_id.nil?
32
+ Legion::Transport::Messages::TaskLog.new(
33
+ task_id: task_id,
34
+ runner_class: to_s,
35
+ entry: {
36
+ exception: true,
37
+ message: exception.message,
38
+ **opts
39
+ }
40
+ ).publish
41
+ end
42
+
43
+ raise Legion::Exception::HandledTask
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/transport'
4
+ require 'legion/transport/messages/task_update'
5
+ require 'legion/transport/messages/task_log'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Helpers
10
+ module Task
11
+ def generate_task_log(task_id:, runner_class: to_s, function:, **payload)
12
+ Legion::Transport::Messages::TaskLog.new(task_id: task_id, runner_class: runner_class, function: function, entry: payload).publish
13
+ end
14
+
15
+ def task_update(task_id, status, **opts)
16
+ return if task_id.nil? || status.nil?
17
+
18
+ update_hash = { task_id: task_id, status: status }
19
+ %i[results payload function_args payload results].each do |column|
20
+ update_hash[column] = opts[column] if opts.key? column
21
+ end
22
+ Legion::Transport::Messages::TaskUpdate.new(update_hash).publish
23
+ rescue StandardError => e
24
+ log.fatal e.message
25
+ log.fatal e.backtrace
26
+ raise e
27
+ end
28
+
29
+ def generate_task_id(function_id:, status: 'task.queued', **opts)
30
+ insert = { status: status, function_id: function_id }
31
+ insert[:payload] = Legion::JSON.dump(opts[:payload]) if opts.key? :payload
32
+ insert[:function_args] = Legion::JSON.dump(opts[:args]) if opts.key? :args
33
+ %i[master_id parent_id relationship_id task_id].each do |column|
34
+ insert[column] = opts[column] if opts.key? column
35
+ end
36
+
37
+ { success: true, task_id: Legion::Data::Model::Task.insert(insert), **insert }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Helpers
8
+ module Transport
9
+ include Legion::Extensions::Helpers::Base
10
+
11
+ def transport_path
12
+ @transport_path ||= "#{full_path}/transport"
13
+ end
14
+
15
+ def transport_class
16
+ @transport_class ||= lex_class::Transport
17
+ end
18
+
19
+ def messages
20
+ @messages ||= transport_class::Messages
21
+ end
22
+
23
+ def queues
24
+ @queues ||= transport_class::Queues
25
+ end
26
+
27
+ def exchanges
28
+ @exchanges ||= transport_class::Exchanges
29
+ end
30
+
31
+ def default_exchange
32
+ @default_exchange ||= build_default_exchange
33
+ end
34
+
35
+ def build_default_exchange
36
+ exchange = "#{transport_class}::Exchanges::#{lex_const}"
37
+ return Object.const_get(exchange) if transport_class::Exchanges.const_defined? lex_const
38
+
39
+ transport_class::Exchanges.const_set(lex_const, Class.new(Legion::Transport::Exchange))
40
+ @default_exchange = Kernel.const_get(exchange)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Transport
6
+ include Legion::Extensions::Helpers::Transport
7
+
8
+ attr_accessor :exchanges, :queues, :consumers, :messages
9
+ def build
10
+ @queues = []
11
+ @exchanges = []
12
+ @messages = []
13
+ @consumers = []
14
+ generate_base_modules
15
+ require_transport_items
16
+
17
+ build_e_to_e
18
+ build_e_to_q(e_to_q)
19
+ build_e_to_q(additional_e_to_q)
20
+ auto_create_dlx_exchange
21
+ auto_create_dlx_queue
22
+ rescue StandardError => e
23
+ Legion::Logging.error e.message
24
+ Legion::Logging.error e.backtrace
25
+ end
26
+
27
+ def generate_base_modules
28
+ lex_class.const_set('Transport', Module.new) unless lex_class.const_defined?('Transport')
29
+ %w[Queues Exchanges Messages Consumers].each do |thing|
30
+ next if transport_class.const_defined? thing
31
+
32
+ transport_class.const_set(thing, Module.new)
33
+ end
34
+ end
35
+
36
+ def require_transport_items
37
+ { 'exchanges': @exchanges, 'queues': @queues, 'consumers': @consumers, 'messages': @messages }.each do |item, obj|
38
+ Dir[File.expand_path("#{transport_path}/#{item}/*.rb")].sort.each do |file|
39
+ require file
40
+ file_name = file.to_s.split('/').last.split('.').first
41
+ obj.push(file_name) unless obj.include?(file_name)
42
+ end
43
+ end
44
+ end
45
+
46
+ def auto_create_exchange(exchange, default_exchange = false)
47
+ if Object.const_defined? exchange
48
+ Legion::Logging.warn "#{exchange} is already defined"
49
+ return
50
+ end
51
+ return build_default_exchange if default_exchange
52
+
53
+ transport_class::Exchanges.const_set(exchange.split('::').pop, Class.new(Legion::Transport::Exchange) do
54
+ def exchange_name
55
+ self.class.ancestors.first.to_s.split('::')[5].downcase
56
+ end
57
+ end)
58
+ end
59
+
60
+ def auto_create_queue(queue)
61
+ if Kernel.const_defined?(queue)
62
+ Legion::Logging.warn "#{queue} is already defined"
63
+ return
64
+ end
65
+
66
+ Kernel.const_set(queue, Class.new(Legion::Transport::Queue))
67
+ end
68
+
69
+ def auto_create_dlx_exchange
70
+ return if transport_class::Exchanges.const_defined? 'Dlx'
71
+
72
+ dlx = transport_class::Exchanges.const_set('Dlx', Class.new(default_exchange) do
73
+ def exchange_name
74
+ "#{super}.dlx"
75
+ end
76
+ end)
77
+ dlx.new
78
+ dlx
79
+ end
80
+
81
+ def auto_create_dlx_queue
82
+ return if transport_class::Queues.const_defined?('Dlx')
83
+
84
+ special_name = default_exchange.new.exchange_name
85
+ dlx_queue = Legion::Transport::Queue.new "#{special_name}.dlx"
86
+ dlx_queue.bind("#{special_name}.dlx", { routing_key: '#' })
87
+ end
88
+
89
+ def build_e_to_q(array)
90
+ array.each do |binding|
91
+ binding[:routing_key] = nil unless binding.key? :routing_key
92
+ binding[:to] = nil unless binding.key? :to
93
+ bind_e_to_q(**binding)
94
+ end
95
+ end
96
+
97
+ def bind_e_to_q(to:, from: default_exchange, routing_key: nil)
98
+ if from.is_a? String
99
+ from = "#{transport_class}::Exchanges::#{from.split('_').collect(&:capitalize).join}" unless from.include?('::')
100
+ auto_create_exchange(from) unless Object.const_defined? from
101
+ end
102
+
103
+ if to.is_a? String
104
+ to = "#{transport_class}::Queues::#{to.split('_').collect(&:capitalize).join}" unless to.include?('::')
105
+ auto_create_queue(to) unless Object.const_defined?(to)
106
+ end
107
+
108
+ routing_key = to.to_s.split('::').last.downcase if routing_key.nil?
109
+ bind(from, to, routing_key: routing_key)
110
+ end
111
+
112
+ def build_e_to_e
113
+ e_to_e.each do |binding|
114
+ if binding[:from].is_a? String
115
+ binding[:from] = "#{transport_class}::Exchanges::#{binding[:from].capitalize}" unless binding[:from].include?('::')
116
+ auto_create_exchange(binding[:from]) unless Object.const_defined? binding[:from]
117
+ end
118
+
119
+ if binding[:to].is_a? String
120
+ binding[:to] = "#{transport_class}::Exchanges::#{binding[:to].capitalize}" unless binding[:to].include?('::')
121
+ auto_create_exchange(binding[:to]) unless Object.const_defined? binding[:to]
122
+ end
123
+
124
+ bind(binding[:from], binding[:to], binding)
125
+ end
126
+ end
127
+
128
+ def bind(from, to, routing_key: nil, **_options)
129
+ from = from.is_a?(String) ? Kernel.const_get(from).new : from.new
130
+ to = to.is_a?(String) ? Kernel.const_get(to).new : to.new
131
+ to.bind(from, routing_key: routing_key)
132
+ rescue StandardError => e
133
+ Legion::Logging.fatal e.message
134
+ Legion::Logging.fatal e.backtrace
135
+ Legion::Logging.fatal({ from: from, to: to, routing_key: routing_key })
136
+ end
137
+
138
+ def e_to_q
139
+ [] if !@exchanges.count != 1
140
+ auto = []
141
+ @queues.each do |queue|
142
+ auto.push(from: @exchanges.first, to: queue, routing_key: queue)
143
+ end
144
+ auto
145
+ end
146
+
147
+ def e_to_e
148
+ []
149
+ end
150
+
151
+ def additional_e_to_q
152
+ []
153
+ end
154
+ end
155
+ end
156
+ end
@@ -42,6 +42,9 @@ module Legion
42
42
  end
43
43
 
44
44
  def run!
45
+ start_time = Time.now
46
+ @options[:time_limit] = @options[:time_limit].to_i if @options.key? :time_limit
47
+ @quit = false
45
48
  check_pid
46
49
  daemonize if daemonize?
47
50
  write_pid
@@ -49,12 +52,14 @@ module Legion
49
52
 
50
53
  until quit
51
54
  sleep(1) # in real life, something productive would happen here
55
+ if @options.key? :time_limit
56
+ @quit = true if Time.now - start_time > @options[:time_limit]
57
+ end
52
58
  end
53
- # sleep(1)
54
- # sleep(1)
55
59
  Legion::Logging.info('Legion is shutting down!')
56
- # @service.shutdown
60
+ Legion.shutdown
57
61
  Legion::Logging.info('Legion has shutdown. Goodbye!')
62
+
58
63
  exit
59
64
  end
60
65
 
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/exceptions/handled_task'
4
+ require_relative 'runner/log'
5
+ require_relative 'runner/status'
6
+ require 'legion/transport'
7
+ require 'legion/transport/messages/check_subtask'
8
+
9
+ module Legion
10
+ module Runner
11
+ def self.run(runner_class:, function:, task_id: nil, args: nil, check_subtask: true, generate_task: true, parent_id: nil, master_id: nil, **opts) # rubocop:disable Metrics/ParameterLists
12
+ runner_class = Kernel.const_get(runner_class) if runner_class.is_a? String
13
+
14
+ if task_id.nil? && generate_task
15
+ task_gen = Legion::Runner::Status.generate_task_id(
16
+ function: function,
17
+ runner_class: runner_class,
18
+ parent_id: parent_id, master_id: master_id, task_id: task_id, **opts
19
+ )
20
+ task_id = task_gen[:task_id] unless task_gen.nil?
21
+ end
22
+
23
+ args = opts if args.nil?
24
+ args[:task_id] = task_id unless task_id.nil?
25
+ args[:master_id] = master_id unless master_id.nil?
26
+ args[:parent_id] = parent_id unless parent_id.nil?
27
+
28
+ result = runner_class.send(function, args)
29
+ rescue Legion::Exception::HandledTask
30
+ status = 'task.exception'
31
+ result = { error: {} }
32
+ rescue StandardError => e
33
+ runner_class.handle_exception(e,
34
+ **opts,
35
+ runner_class: runner_class,
36
+ args: args,
37
+ function: function,
38
+ task_id: task_id,
39
+ generate_task: generate_task,
40
+ check_subtask: check_subtask)
41
+ status = 'task.exception'
42
+ result = { success: false, status: status, error: { message: e.message, backtrace: e.backtrace } }
43
+ else
44
+ status = 'task.completed'
45
+ ensure
46
+ Legion::Runner::Status.update(task_id: task_id, status: status) unless task_id.nil?
47
+ if check_subtask && status == 'task.completed'
48
+ Legion::Transport::Messages::CheckSubtask.new(runner_class: runner_class,
49
+ function: function,
50
+ result: result,
51
+ original_args: args,
52
+ **opts).publish
53
+ end
54
+ return { success: true, status: status, result: result, task_id: task_id } # rubocop:disable Lint/EnsureReturn
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Runner
5
+ module Log
6
+ def self.exception(exc, **opts)
7
+ Legion::Logging.error exc.message
8
+ Legion::Logging.error opts
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Runner
5
+ module Status
6
+ def self.update(task_id:, status: 'task.completed', **opts)
7
+ Legion::Logging.debug "Legion::Runner::Status.update called, #{task_id}, status: #{status}, #{opts}"
8
+ return if status.nil?
9
+
10
+ if Legion::Settings[:data][:connected]
11
+ update_db(task_id: task_id, status: status, **opts)
12
+ else
13
+ update_rmq(task_id: task_id, status: status, **opts)
14
+ end
15
+ end
16
+
17
+ def self.update_rmq(task_id:, status: 'task.completed', **opts)
18
+ return if status.nil?
19
+
20
+ Legion::Transport::Messages::TaskUpdate.new(task_id: task_id, status: status, **opts).publish
21
+ rescue StandardError => e
22
+ Legion::Logging.fatal e.message
23
+ Legion::Logging.fatal e.backtrace
24
+ retries ||= 0
25
+ Legion::Logging.fatal 'Will retry in 3 seconds' if retries < 5
26
+ sleep(3)
27
+ retry if (retries += 1) < 5
28
+ end
29
+
30
+ def self.update_db(task_id:, status: 'task.completed', **opts)
31
+ return if status.nil?
32
+
33
+ task = Legion::Data::Model::Task[task_id]
34
+ task.update(status: status)
35
+ rescue StandardError => e
36
+ Legion::Logging.warn e.message
37
+ Legion::Logging.warn 'Legion::Runner.update_status_db failed, defaulting to rabbitmq'
38
+ Legion::Logging.warn e.backtrace
39
+ update_rmq(task_id: task_id, status: status, **opts)
40
+ end
41
+
42
+ def self.generate_task_id(runner_class:, function:, status: 'task.queued', **opts)
43
+ Legion::Logging.debug "Legion::Runner::Status.generate_task_id called, #{runner_class}, #{function}, status: #{status}, #{opts}"
44
+ return nil unless Legion::Settings[:data][:connected]
45
+
46
+ runner = Legion::Data::Model::Runner.where(namespace: runner_class.to_s.downcase).first
47
+ return nil if runner.nil?
48
+
49
+ function = Legion::Data::Model::Function.where(runner_id: runner.values[:id], name: function).first
50
+ return nil if function.nil?
51
+
52
+ insert = { status: status, function_id: function.values[:id] }
53
+ insert[:parent_id] = opts[:task_id] if opts.key? :task_id
54
+ insert[:master_id] = opts[:task_id] if opts.key? :task_id
55
+ insert[:payload] = Legion::JSON.dump(opts[:payload]) if opts.key? :payload
56
+
57
+ %i[function_args master_id parent_id relationship_id].each do |column|
58
+ next unless opts.key? column
59
+
60
+ insert[column] = opts[column].is_a?(Hash) ? Legion::JSON.dump(opts[column]) : opts[column]
61
+ # insert[column] = opts[column] if opts.key? column
62
+ end
63
+
64
+ { success: true, task_id: Legion::Data::Model::Task.insert(insert), **insert }
65
+ rescue StandardError => e
66
+ Legion::Logging.error e.message
67
+ Legion::Logging.error e.backtrace
68
+ raise(e)
69
+ end
70
+ end
71
+ end
72
+ end