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
@@ -1,12 +1,21 @@
1
- # frozen_string_literal: true
2
-
3
1
  Process.setproctitle('Legion')
2
+ require 'concurrent-ruby'
3
+ require 'securerandom'
4
+ # require 'legion/exceptions'
4
5
  require 'legion/version'
5
6
  require 'legion/process'
6
7
  require 'legion/service'
8
+ require 'legion/extensions'
7
9
 
8
- # Base Legion Module to start the world
9
10
  module Legion
10
- Legion::Service.new({})
11
- Legion::Logging.info("Started Legion v#{Legion::VERSION}")
11
+ attr_reader :service
12
+
13
+ def self.start
14
+ @service = Legion::Service.new
15
+ Legion::Logging.info("Started Legion v#{Legion::VERSION}")
16
+ end
17
+
18
+ def self.shutdown
19
+ @service.shutdown
20
+ end
12
21
  end
@@ -0,0 +1,6 @@
1
+ module Legion
2
+ module Exception
3
+ class HandledTask < StandardError
4
+ end
5
+ end
6
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Legion
4
4
  module Exception
5
- # Missing argument exception
6
- class MissingArgument < StandardError; end
5
+ class MissingArgument < StandardError
6
+ end
7
7
  end
8
8
  end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/core'
4
+ require 'legion/runner'
5
+
6
+ module Legion
7
+ module Extensions
8
+ class << self
9
+ def setup
10
+ hook_extensions
11
+ end
12
+
13
+ def hook_extensions
14
+ @timer_tasks = []
15
+ @loop_tasks = []
16
+ @once_tasks = []
17
+ @poll_tasks = []
18
+ @subscription_tasks = []
19
+ @actors = []
20
+
21
+ find_extensions
22
+ load_extensions
23
+ end
24
+
25
+ def shutdown
26
+ @subscription_tasks.each do |task|
27
+ task[:threadpool].shutdown
28
+ task[:threadpool].wait_for_termination(2)
29
+ task[:threadpool].kill
30
+ end
31
+ @loop_tasks.each { |task| task[:running_class].cancel if task[:running_class].respond_to?(:cancel) }
32
+ @once_tasks.each { |task| task[:running_class].cancel if task[:running_class].respond_to?(:cancel) }
33
+ @timer_tasks.each { |task| task[:running_class].cancel if task[:running_class].respond_to?(:cancel) }
34
+ @poll_tasks.each { |task| task[:running_class].cancel if task[:running_class].respond_to?(:cancel) }
35
+
36
+ Legion::Logging.info 'Successfully shut down all actors'
37
+ end
38
+
39
+ def load_extensions
40
+ @extensions ||= {}
41
+ @loaded_extensions ||= []
42
+ @extensions.each do |extension, values|
43
+ if values.key(:enabled) && !values[:enabled]
44
+ Legion::Logging.info "Skipping #{extension} because it's disabled"
45
+ next
46
+ end
47
+
48
+ if Legion::Settings[:extensions].key?(extension.to_sym) && Legion::Settings[:extensions][extension.to_sym].key?(:enabled) && !Legion::Settings[:extensions][extension.to_sym][:enabled] # rubocop:disable Layout/LineLength
49
+ next
50
+ end
51
+
52
+ unless load_extension(extension, values)
53
+ Legion::Logging.warn("#{extension} failed to load")
54
+ next
55
+ end
56
+ @loaded_extensions.push(extension)
57
+ end
58
+ Legion::Logging.info "#{@extensions.count} extensions loaded with subscription:#{@subscription_tasks.count},every:#{@timer_tasks.count},poll:#{@poll_tasks.count},once:#{@once_tasks.count},loop:#{@loop_tasks.count}"
59
+ end
60
+
61
+ def load_extension(extension, values)
62
+ return unless gem_load(values[:gem_name], extension)
63
+
64
+ extension = Kernel.const_get(values[:extension_class])
65
+ has_logger = extension.respond_to?(:log)
66
+ extension.autobuild
67
+
68
+ require 'legion/transport/messages/lex_register'
69
+ Legion::Transport::Messages::LexRegister.new(function: 'save', opts: extension.runners).publish
70
+
71
+ if extension.respond_to?(:meta_actors) && extension.meta_actors.is_a?(Array)
72
+ extension.meta_actors.each do |_key, actor|
73
+ extension.log.debug("hooking meta actor: #{actor}") if has_logger
74
+ hook_actor(**actor)
75
+ end
76
+ end
77
+
78
+ extension.actors.each do |_key, actor|
79
+ extension.log.debug("hooking literal actor: #{actor}") if has_logger
80
+ hook_actor(**actor)
81
+ end
82
+ extension.log.info 'Loaded'
83
+ rescue StandardError => e
84
+ Legion::Logging.error e.message
85
+ Legion::Logging.error e.backtrace
86
+ false
87
+ end
88
+
89
+ def hook_actor(extension:, extension_name:, actor_class:, size: 1, **opts)
90
+ size = 1 unless size.is_a? Integer
91
+ extension_hash = {
92
+ extension: extension,
93
+ extension_name: extension_name,
94
+ actor_class: actor_class,
95
+ size: size,
96
+ **opts
97
+ }
98
+ extension_hash[:running_class] = if actor_class.ancestors.include? Legion::Extensions::Actors::Subscription
99
+ actor_class
100
+ else
101
+ actor_class.new
102
+ end
103
+
104
+ if actor_class.ancestors.include? Legion::Extensions::Actors::Every
105
+ @timer_tasks.push(extension_hash)
106
+ elsif actor_class.ancestors.include? Legion::Extensions::Actors::Once
107
+ @once_tasks.push(extension_hash)
108
+ elsif actor_class.ancestors.include? Legion::Extensions::Actors::Loop
109
+ @loop_tasks.push(extension_hash)
110
+ elsif actor_class.ancestors.include? Legion::Extensions::Actors::Poll
111
+ @poll_tasks.push(extension_hash)
112
+ elsif actor_class.ancestors.include? Legion::Extensions::Actors::Subscription
113
+ extension_hash[:threadpool] = Concurrent::FixedThreadPool.new(100)
114
+ size.times do
115
+ extension_hash[:threadpool].post do
116
+ actor_class.new.async.subscribe
117
+ end
118
+ end
119
+ @subscription_tasks.push(extension_hash)
120
+ else
121
+ Legion::Logging.fatal 'did not match any actor classes'
122
+ end
123
+ end
124
+
125
+ def gem_load(gem_name, name)
126
+ gem_path = "#{Gem::Specification.find_by_name(gem_name).gem_dir}/lib/legion/extensions/#{name}"
127
+ require gem_path
128
+ true
129
+ rescue LoadError => e
130
+ Legion::Logging.error e.message
131
+ Legion::Logging.error e.backtrace
132
+ Legion::Logging.error "gem_path: #{gem_path}" unless gem_path.nil?
133
+ false
134
+ end
135
+
136
+ def find_extensions
137
+ @extensions ||= {}
138
+ Gem::Specification.all_names.each do |gem|
139
+ next unless gem[0..3] == 'lex-'
140
+
141
+ lex = gem.split('-')
142
+ @extensions[lex[1]] = { full_gem_name: gem,
143
+ gem_name: "lex-#{lex[1]}",
144
+ extension_name: lex[1],
145
+ version: lex[2],
146
+ extension_class: "Legion::Extensions::#{lex[1].capitalize}" }
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hashdiff'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Actors
8
+ module Base
9
+ include Legion::Extensions::Helpers::Lex
10
+
11
+ def runner
12
+ # runner_class:, function:, task_id: nil, args: nil, check_subtask: true, generate_task: true, parent_id: nil, master_id: nil, **opts
13
+ Legion::Runner.run(runner_class: runner_class, function: function, check_subtask: check_subtask, generate_task: generate_task)
14
+ rescue StandardError => e
15
+ Legion::Logging.error e.message
16
+ Legion::Logging.error e.backtrace
17
+ end
18
+
19
+ def manual
20
+ # Legion::Runner.run(runner_class: runner_class, function: runner_function, check_subtask: false, generate_task: false, args: args)
21
+ runner_class.send(runner_function, args)
22
+ rescue StandardError => e
23
+ Legion::Logging.error e.message
24
+ Legion::Logging.error e.backtrace
25
+ end
26
+
27
+ def function
28
+ nil
29
+ end
30
+
31
+ def use_runner?
32
+ true
33
+ end
34
+
35
+ def args
36
+ {}
37
+ end
38
+
39
+ def check_subtask?
40
+ true
41
+ end
42
+
43
+ def generate_task?
44
+ false
45
+ end
46
+
47
+ def enabled?
48
+ true
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Actors
8
+ class Every
9
+ include Legion::Extensions::Actors::Base
10
+
11
+ def initialize(**_opts)
12
+ @timer = Concurrent::TimerTask.new(execution_interval: time, timeout_interval: timeout, run_now: run_now?) do
13
+ use_runner? ? runner : manual
14
+ end
15
+
16
+ @timer.execute
17
+ rescue StandardError => e
18
+ Legion::Logging.error e.message
19
+ Legion::Logging.error e.backtrace
20
+ end
21
+
22
+ def time
23
+ 1
24
+ end
25
+
26
+ def timeout
27
+ 5
28
+ end
29
+
30
+ def run_now?
31
+ false
32
+ end
33
+
34
+ def action(**_opts)
35
+ Legion::Logging.warn 'An extension is using the default block from Legion::Extensions::Runners::Every'
36
+ end
37
+
38
+ def cancel
39
+ Legion::Logging.debug 'Cancelling Legion Timer'
40
+ return true unless @timer.respond_to?(:shutdown)
41
+
42
+ @timer.shutdown
43
+ rescue StandardError => e
44
+ Legion::Logging.error e.message
45
+ Legion::Logging.error e.backtrace
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Actors
8
+ class Loop
9
+ include Concurrent::Async
10
+ include Legion::Extensions::Actors::Base
11
+
12
+ def initialize
13
+ @loop = true
14
+ async.run
15
+ rescue StandardError => e
16
+ Legion::Logging.error e
17
+ Legion::Logging.error e.backtrace
18
+ end
19
+
20
+ def run
21
+ action while @loop
22
+ end
23
+
24
+ def action(**_opts)
25
+ Legion::Logging.warn 'An extension is using the default action for a loop'
26
+ end
27
+
28
+ def cancel
29
+ @loop = false
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'base'
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Actors
6
+ class Nothing
7
+ include Legion::Extensions::Actors::Base
8
+
9
+ def initialize; end
10
+
11
+ def cancel; end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Actors
8
+ class Once
9
+ include Legion::Extensions::Actors::Base
10
+
11
+ def initialize
12
+ return if disabled?
13
+
14
+ if respond_to? :functions
15
+ functions.each do
16
+ function
17
+ @task = Concurrent::ScheduledTask.execute(delay) do
18
+ use_runner ? runner : manual
19
+ end
20
+ end
21
+ else
22
+ @task = Concurrent::ScheduledTask.execute(delay) do
23
+ use_runner ? runner : manual
24
+ end
25
+ end
26
+ rescue StandardError => e
27
+ Legion::Logging.error e
28
+ end
29
+
30
+ def delay
31
+ 1.0
32
+ end
33
+
34
+ def cancel
35
+ return if disabled?
36
+
37
+ @task.cancel unless @task.cancelled?
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'hashdiff'
5
+ require 'time'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Actors
10
+ class Poll
11
+ include Legion::Extensions::Actors::Base
12
+
13
+ def initialize # rubocop:disable Metrics/AbcSize
14
+ log.debug "Starting timer for #{self.class} with #{{ execution_interval: time, timeout_interval: timeout, run_now: run_now?, check_subtask: check_subtask? }}"
15
+ @timer = Concurrent::TimerTask.new(execution_interval: time, timeout_interval: timeout, run_now: run_now?) do
16
+ t1 = Time.now
17
+ log.debug "Running #{self.class}"
18
+ old_result = Legion::Cache.get(cache_name)
19
+ log.debug "Cached value for #{self.class}: #{old_result}"
20
+ results = Legion::JSON.load(Legion::JSON.dump(manual))
21
+ Legion::Cache.set(cache_name, results, time * 2)
22
+
23
+ unless old_result.nil?
24
+ results[:diff] = Hashdiff.diff(results, old_result, numeric_tolerance: 0.0, array_path: false) do |_path, obj1, obj2|
25
+ if int_percentage_normalize.positive? && obj1.is_a?(Integer) && obj2.is_a?(Integer)
26
+ obj1.between?(obj2 * (1 - int_percentage_normalize), obj2 * (1 + int_percentage_normalize))
27
+ end
28
+ end
29
+ results[:changed] = results[:diff].count.positive?
30
+
31
+ Legion::Logging.info results[:diff] if results[:changed]
32
+ Legion::Transport::Messages::CheckSubtask.new(runner_class: runner_class.to_s,
33
+ function: runner_function,
34
+ result: results,
35
+ type: 'poll_result',
36
+ polling: true).publish
37
+ end
38
+
39
+ sleep_time = 1 - (Time.now - t1)
40
+ sleep(sleep_time) if sleep_time.positive?
41
+ log.debug("#{self.class} result: #{results}")
42
+ results
43
+ rescue StandardError => e
44
+ Legion::Logging.fatal e.message
45
+ Legion::Logging.fatal e.backtrace
46
+ end
47
+ @timer.execute
48
+ rescue StandardError => e
49
+ Legion::Logging.error e.message
50
+ Legion::Logging.error e.backtrace
51
+ end
52
+
53
+ def cache_name
54
+ lex_name.to_s + '_' + runner_name
55
+ end
56
+
57
+ def int_percentage_normalize
58
+ 0.00
59
+ end
60
+
61
+ def time
62
+ 9
63
+ end
64
+
65
+ def run_now?
66
+ true
67
+ end
68
+
69
+ def check_subtask?
70
+ true
71
+ end
72
+
73
+ def timeout
74
+ 5
75
+ end
76
+
77
+ def action(_payload = {})
78
+ Legion::Logging.warn 'An extension is using the default block from Legion::Extensions::Runners::Every'
79
+ end
80
+
81
+ def cancel
82
+ Legion::Logging.debug 'Cancelling Legion Poller'
83
+ @timer.shutdown
84
+ rescue StandardError => e
85
+ Legion::Logging.error e.message
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end