legionio 0.1.1 → 0.2.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.
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