legionio 0.3.3 → 0.4.2

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +136 -0
  3. data/.gitignore +15 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +94 -0
  6. data/CHANGELOG.md +43 -0
  7. data/Dockerfile +9 -0
  8. data/Gemfile +3 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +161 -0
  11. data/Rakefile +32 -0
  12. data/bitbucket-pipelines.yml +19 -0
  13. data/docker_deploy.rb +10 -0
  14. data/exe/legion +4 -0
  15. data/exe/legionio +53 -0
  16. data/exe/lex_gen +5 -0
  17. data/legionio.gemspec +64 -0
  18. data/lib/legion.rb +25 -0
  19. data/lib/legion/cli.rb +56 -0
  20. data/lib/legion/cli/chain.rb +35 -0
  21. data/lib/legion/cli/cohort.rb +10 -0
  22. data/lib/legion/cli/function.rb +41 -0
  23. data/lib/legion/cli/lex/actor.rb +31 -0
  24. data/lib/legion/cli/lex/exchange.rb +32 -0
  25. data/lib/legion/cli/lex/message.rb +32 -0
  26. data/lib/legion/cli/lex/queue.rb +45 -0
  27. data/lib/legion/cli/lex/runner.rb +70 -0
  28. data/lib/legion/cli/lex/templates/actor.erb +6 -0
  29. data/lib/legion/cli/lex/templates/actor_spec.erb +0 -0
  30. data/lib/legion/cli/lex/templates/base/bitbucket.yml.erb +69 -0
  31. data/lib/legion/cli/lex/templates/base/gemfile.erb +3 -0
  32. data/lib/legion/cli/lex/templates/base/gemspec.erb +26 -0
  33. data/lib/legion/cli/lex/templates/base/gitignore.erb +11 -0
  34. data/lib/legion/cli/lex/templates/base/lex.erb +9 -0
  35. data/lib/legion/cli/lex/templates/base/lex_spec.erb +5 -0
  36. data/lib/legion/cli/lex/templates/base/lic.erb +21 -0
  37. data/lib/legion/cli/lex/templates/base/rakefile.erb +6 -0
  38. data/lib/legion/cli/lex/templates/base/readme.md.erb +2 -0
  39. data/lib/legion/cli/lex/templates/base/rubocop.yml.erb +15 -0
  40. data/lib/legion/cli/lex/templates/base/spec_helper.rb.erb +11 -0
  41. data/lib/legion/cli/lex/templates/base/version.erb +7 -0
  42. data/lib/legion/cli/lex/templates/exchange.erb +11 -0
  43. data/lib/legion/cli/lex/templates/exchange_spec.erb +0 -0
  44. data/lib/legion/cli/lex/templates/message.erb +23 -0
  45. data/lib/legion/cli/lex/templates/message_spec.erb +0 -0
  46. data/lib/legion/cli/lex/templates/queue.erb +12 -0
  47. data/lib/legion/cli/lex/templates/queue_helper.erb +24 -0
  48. data/lib/legion/cli/lex/templates/queue_spec.erb +11 -0
  49. data/lib/legion/cli/lex/templates/runner.erb +11 -0
  50. data/lib/legion/cli/lex/templates/runner_spec.erb +11 -0
  51. data/lib/legion/cli/relationship.rb +22 -0
  52. data/lib/legion/cli/task.rb +49 -0
  53. data/lib/legion/cli/trigger.rb +88 -0
  54. data/lib/legion/cli/version.rb +5 -0
  55. data/lib/legion/extensions.rb +219 -0
  56. data/lib/legion/extensions/actors/base.rb +47 -0
  57. data/lib/legion/extensions/actors/every.rb +48 -0
  58. data/lib/legion/extensions/actors/loop.rb +32 -0
  59. data/lib/legion/extensions/actors/nothing.rb +15 -0
  60. data/lib/legion/extensions/actors/once.rb +40 -0
  61. data/lib/legion/extensions/actors/poll.rb +87 -0
  62. data/lib/legion/extensions/actors/subscription.rb +139 -0
  63. data/lib/legion/extensions/builders/actors.rb +61 -0
  64. data/lib/legion/extensions/builders/base.rb +36 -0
  65. data/lib/legion/extensions/builders/helpers.rb +24 -0
  66. data/lib/legion/extensions/builders/runners.rb +58 -0
  67. data/lib/legion/extensions/core.rb +131 -0
  68. data/lib/legion/extensions/data.rb +58 -0
  69. data/lib/legion/extensions/data/migrator.rb +28 -0
  70. data/lib/legion/extensions/data/model.rb +8 -0
  71. data/lib/legion/extensions/helpers/base.rb +82 -0
  72. data/lib/legion/extensions/helpers/cache.rb +23 -0
  73. data/lib/legion/extensions/helpers/core.rb +41 -0
  74. data/lib/legion/extensions/helpers/data.rb +23 -0
  75. data/lib/legion/extensions/helpers/lex.rb +48 -0
  76. data/lib/legion/extensions/helpers/logger.rb +44 -0
  77. data/lib/legion/extensions/helpers/task.rb +60 -0
  78. data/lib/legion/extensions/helpers/transport.rb +44 -0
  79. data/lib/legion/extensions/transport.rb +159 -0
  80. data/lib/legion/lex.rb +89 -0
  81. data/lib/legion/process.rb +124 -0
  82. data/lib/legion/runner.rb +55 -0
  83. data/lib/legion/runner/log.rb +10 -0
  84. data/lib/legion/runner/status.rb +69 -0
  85. data/lib/legion/service.rb +130 -0
  86. data/lib/legion/supervision.rb +15 -0
  87. data/lib/legion/version.rb +3 -0
  88. metadata +243 -39
@@ -0,0 +1,22 @@
1
+ module Legion
2
+ class Cli
3
+ class Relationship < Thor
4
+ desc 'create', 'creates a new relationship'
5
+ def create(_name, _type)
6
+ trigger_id = invoke('legion:cli:function:find', [], internal: true, capture: true) # rubocop:disable Lint/UselessAssignment
7
+ end
8
+
9
+ desc 'activate', 'actives a relationship'
10
+ def active; end
11
+
12
+ desc 'deactivate', 'deactivates a relationship'
13
+ def deactivate; end
14
+
15
+ desc 'modify', 'modify an existing relationship'
16
+ def modify; end
17
+
18
+ desc 'delete', 'deletes a relationship'
19
+ def delete; end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,49 @@
1
+ module Legion
2
+ class Cli
3
+ class Task < Thor
4
+ package_name 'Legion'
5
+
6
+ option :limit, type: :numeric, required: true, default: 10, desc: 'how many tasks to return'
7
+ desc 'show', 'show'
8
+ option :status, type: :string, required: false, desc: 'search for specific status'
9
+ def show
10
+ Legion::Service.new(cache: false, crypt: false, extensions: false, log_level: 'error')
11
+ rows = [%w[id relationship function status]]
12
+ Legion::Data::Model::Task.limit(options[:limit]).order(:id).reverse_each do |row|
13
+ rows.push([row.values[:id], row.values[:relationship_id], row.values[:function_id], row.values[:status]])
14
+ end
15
+
16
+ print_table rows
17
+ end
18
+
19
+ desc 'test', 'test'
20
+ def status(id)
21
+ Legion::Service.new(cache: false, crypt: false, extensions: false, log_level: 'error')
22
+ say Legion::Data::Model::Task[id].values
23
+ end
24
+
25
+ desc 'logs', 'logs'
26
+ option :limit, type: :numeric, required: true, default: 10, desc: 'how many tasks to return'
27
+ def logs(id)
28
+ Legion::Service.new(cache: false, crypt: false, extensions: false, log_level: 'error')
29
+ rows = [%w[id node_id created entry]]
30
+ Legion::Data::Model::TaskLog.where(task_id: id).limit(options[:limit]).each do |row|
31
+ rows.push([row.values[:id], row.values[:node_id], row.values[:created], row.values[:entry]])
32
+ end
33
+ print_table rows
34
+ end
35
+
36
+ desc 'purge', 'purge'
37
+ def purge
38
+ Legion::Service.new(cache: false, crypt: false, extensions: false, log_level: 'error')
39
+ days = ask 'how many days do you want to keep?', default: 7
40
+ dataset = Legion::Data::Model::Task.where { created < DateTime.now - days.to_i }
41
+ yes? "This will delete #{dataset.count} tasks, continue?", :red
42
+ dataset.delete
43
+ say 'Done!'
44
+ end
45
+
46
+ default_task :show
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,88 @@
1
+ module Legion
2
+ class Cli
3
+ class Trigger < Thor
4
+ desc 'queue', 'used to send a job directly to a worker via Legion::Transport'
5
+ option :extension, type: :string, required: false, desc: 'extension short name'
6
+ option :runner, type: :string, required: false, desc: 'runner short name'
7
+ option :function, type: :string, required: false, desc: 'function short name'
8
+ option :delay, type: :numeric, default: 0, desc: 'how long to wait before running the task'
9
+ def queue(*args) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
10
+ Legion::Service.new(cache: false, crypt: false, extensions: false, log_level: 'error')
11
+ include Legion::Extensions::Helpers::Task
12
+ response = if options['extension'].is_a? String
13
+ options[:extension]
14
+ else
15
+ ask 'trigger extension?', limited_to: Legion::Data::Model::Extension.map(:name)
16
+ end
17
+ trigger_extension = Legion::Data::Model::Extension.where(name: response).first
18
+ runners = Legion::Data::Model::Runner.where(extension_id: trigger_extension.values[:id])
19
+ if runners.count == 1
20
+ trigger_runner = runners.first
21
+ say "Auto selecting #{trigger_runner.values[:name]} since it is the only option for runners"
22
+ else
23
+ response = options[:runner].is_a?(String) ? options[:runner] : ask('trigger runner?', limited_to: runners.map(:name))
24
+ trigger_runner = Legion::Data::Model::Runner.where(name: response).where(extension_id: trigger_extension.values[:id]).first
25
+ end
26
+
27
+ functions = Legion::Data::Model::Function.where(runner_id: trigger_runner.values[:id])
28
+
29
+ if functions.count == 1
30
+ trigger_function = functions.first
31
+ say "Auto selecting #{trigger_function.values[:name]} since it is the only option for functions"
32
+ else
33
+ response = if options[:function].is_a?(String)
34
+ options[:function]
35
+ else
36
+ ask('trigger function?',
37
+ limited_to: Legion::Data::Model::Function.where(runner_id: trigger_runner.values[:id]).map(:name))
38
+ end
39
+ trigger_function = Legion::Data::Model::Function.where(runner_id: trigger_runner.values[:id]).where(name: response).first
40
+ end
41
+ say "#{trigger_runner.values[:namespace]}.#{trigger_function.values[:name]} selected as trigger", :green, :italicized
42
+ payload = {}
43
+ auto_opts = {}
44
+ unless args.count.zero?
45
+ args.each do |arg|
46
+ test = arg.split(':')
47
+ auto_opts[test[0].to_sym] = test[1]
48
+ end
49
+ end
50
+
51
+ Legion::JSON.load(trigger_function.values[:args]).each do |arg, required|
52
+ next if %w[args payload opts options].include? arg.to_s
53
+
54
+ if auto_opts.key? arg
55
+ payload[arg.to_sym] = auto_opts[arg]
56
+ next
57
+ end
58
+ response = ask "#{required == 'keyreq' ? '[required]' : '[optional]'} #{arg} value:"
59
+ if response.empty? && required == 'keyreq'
60
+ say "Error! #{arg} is required and cannot be empty", :red
61
+ redo
62
+ end
63
+ payload[arg.to_sym] = response unless response.empty?
64
+ end
65
+
66
+ status = options[:delay].zero? ? 'task.queued' : 'task.delayed'
67
+ task = generate_task_id(function_id: trigger_function.values[:id], status: status, runner_id: trigger_runner.values[:id], args: payload,
68
+ delay: options[:delay])
69
+
70
+ unless options[:delay].zero?
71
+ say "Task: #{task[:task_id]} is queued and will be run in #{options[:delay]}s"
72
+ return true
73
+ end
74
+
75
+ routing_key = "#{trigger_extension.values[:exchange]}.#{trigger_runner.values[:queue]}.#{trigger_function.values[:name]}"
76
+ exchange = Legion::Transport::Messages::Dynamic.new(function: trigger_function.values[:name], function_id: trigger_function.values[:id],
77
+ routing_key: routing_key, args: payload)
78
+ exchange.options[:task_id] = task[:task_id]
79
+ exchange.publish if options[:delay].zero?
80
+
81
+ say "Task: #{task[:task_id]} was queued"
82
+ end
83
+ remove_command :generate_task_id
84
+
85
+ default_task :queue
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,5 @@
1
+ module Legion
2
+ class Cli
3
+ VERSION = '0.2.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,219 @@
1
+ require 'legion/extensions/core'
2
+ require 'legion/runner'
3
+
4
+ module Legion
5
+ module Extensions
6
+ class << self
7
+ def setup
8
+ hook_extensions
9
+ end
10
+
11
+ def hook_extensions
12
+ @timer_tasks = []
13
+ @loop_tasks = []
14
+ @once_tasks = []
15
+ @poll_tasks = []
16
+ @subscription_tasks = []
17
+ @actors = []
18
+
19
+ find_extensions
20
+ load_extensions
21
+ end
22
+
23
+ def shutdown
24
+ return nil if @loaded_extensions.nil?
25
+
26
+ @subscription_tasks.each do |task|
27
+ task[:threadpool].shutdown
28
+ task[:threadpool].kill unless task[:threadpool].wait_for_termination(5)
29
+ end
30
+
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
+ sleep(0.1)
58
+ end
59
+ 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}"
60
+ end
61
+
62
+ def load_extension(extension, values)
63
+ return unless gem_load(values[:gem_name], extension)
64
+
65
+ extension = Kernel.const_get(values[:extension_class])
66
+ if extension.data_required? && Legion::Settings[:data][:connected] == false
67
+ Legion::Logging.warn "#{values[:extension_name]} requires Legion::Data but isn't enabled, skipping"
68
+ return false
69
+ end
70
+
71
+ if extension.cache_required? && Legion::Settings[:cache][:connected] == false
72
+ Legion::Logging.warn "#{values[:extension_name]} requires Legion::Cache but isn't enabled, skipping"
73
+ return false
74
+ end
75
+
76
+ if extension.vault_required? && Legion::Settings[:crypt][:vault][:connected] == false
77
+ Legion::Logging.warn "#{values[:extension_name]} requires Legion::Crypt::Vault but isn't enabled, skipping"
78
+ return false
79
+ end
80
+
81
+ has_logger = extension.respond_to?(:log)
82
+ extension.autobuild
83
+
84
+ require 'legion/transport/messages/lex_register'
85
+ Legion::Transport::Messages::LexRegister.new(function: 'save', opts: extension.runners).publish
86
+
87
+ if extension.respond_to?(:meta_actors) && extension.meta_actors.is_a?(Array)
88
+ extension.meta_actors.each do |_key, actor|
89
+ extension.log.debug("hooking meta actor: #{actor}") if has_logger
90
+ hook_actor(**actor)
91
+ end
92
+ end
93
+
94
+ extension.actors.each do |_key, actor|
95
+ extension.log.debug("hooking literal actor: #{actor}") if has_logger
96
+ hook_actor(**actor)
97
+ end
98
+ extension.log.info "Loaded v#{extension::VERSION}"
99
+ rescue StandardError => e
100
+ Legion::Logging.error e.message
101
+ Legion::Logging.error e.backtrace
102
+ false
103
+ end
104
+
105
+ def hook_actor(extension:, extension_name:, actor_class:, size: 1, **opts)
106
+ size = if Legion::Settings[:extensions].key?(extension_name.to_sym) && Legion::Settings[:extensions][extension_name.to_sym].key?(:workers)
107
+ Legion::Settings[:extensions][extension_name.to_sym][:workers]
108
+ elsif size.is_a? Integer
109
+ size
110
+ else
111
+ 1
112
+ end
113
+
114
+ extension_hash = {
115
+ extension: extension,
116
+ extension_name: extension_name,
117
+ actor_class: actor_class,
118
+ size: size,
119
+ fallback_policy: :abort,
120
+ **opts
121
+ }
122
+ extension_hash[:running_class] = if actor_class.ancestors.include? Legion::Extensions::Actors::Subscription
123
+ actor_class
124
+ else
125
+ actor_class.new
126
+ end
127
+
128
+ return if extension_hash[:running_class].respond_to?(:enabled?) && !extension_hash[:running_class].enabled?
129
+
130
+ if actor_class.ancestors.include? Legion::Extensions::Actors::Every
131
+ @timer_tasks.push(extension_hash)
132
+ elsif actor_class.ancestors.include? Legion::Extensions::Actors::Once
133
+ @once_tasks.push(extension_hash)
134
+ elsif actor_class.ancestors.include? Legion::Extensions::Actors::Loop
135
+ @loop_tasks.push(extension_hash)
136
+ elsif actor_class.ancestors.include? Legion::Extensions::Actors::Poll
137
+ @poll_tasks.push(extension_hash)
138
+ elsif actor_class.ancestors.include? Legion::Extensions::Actors::Subscription
139
+ extension_hash[:threadpool] = Concurrent::FixedThreadPool.new(size)
140
+ size.times do
141
+ extension_hash[:threadpool].post do
142
+ klass = actor_class.new
143
+ if klass.respond_to?(:async)
144
+ klass.async.subscribe
145
+ else
146
+ klass.subscribe
147
+ end
148
+ end
149
+ end
150
+ @subscription_tasks.push(extension_hash)
151
+ else
152
+ Legion::Logging.fatal 'did not match any actor classes'
153
+ end
154
+ end
155
+
156
+ def gem_load(gem_name, name)
157
+ require "#{Gem::Specification.find_by_name(gem_name).gem_dir}/lib/legion/extensions/#{name}"
158
+ true
159
+ rescue LoadError => e
160
+ Legion::Logging.error e.message
161
+ Legion::Logging.error e.backtrace
162
+ Legion::Logging.error "gem_path: #{gem_path}" unless gem_path.nil?
163
+ false
164
+ end
165
+
166
+ def find_extensions
167
+ @extensions ||= {}
168
+ Gem::Specification.all_names.each do |gem|
169
+ next unless gem[0..3] == 'lex-'
170
+
171
+ lex = gem.split('-')
172
+ @extensions[lex[1]] = { full_gem_name: gem,
173
+ gem_name: "lex-#{lex[1]}",
174
+ extension_name: lex[1],
175
+ version: lex[2],
176
+ extension_class: "Legion::Extensions::#{lex[1].split('_').collect(&:capitalize).join}" }
177
+ end
178
+
179
+ enabled = 0
180
+ requested = 0
181
+
182
+ Legion::Settings[:extensions].each do |extension, values|
183
+ next if @extensions.key? extension.to_s
184
+ next if values[:enabled] == false
185
+
186
+ requested += 1
187
+ next if values[:auto_install] == false
188
+ next if ENV['_'].include? 'bundle'
189
+
190
+ Legion::Logging.warn "#{extension} is missing, attempting to install automatically.."
191
+ install = Gem.install("lex-#{extension}", values[:version])
192
+ Legion::Logging.debug(install)
193
+ lex = Gem::Specification.find_by_name("lex-#{extension}")
194
+
195
+ @extensions[extension.to_s] = {
196
+ full_gem_name: "lex-#{extension}-#{lex.version}",
197
+ gem_name: "lex-#{extension}",
198
+ extension_name: extension.to_s,
199
+ version: lex.version,
200
+ extension_class: "Legion::Extensions::#{extension.to_s.split('_').collect(&:capitalize).join}"
201
+ }
202
+
203
+ enabled += 1
204
+
205
+ rescue StandardError, Gem::MissingSpecError => e
206
+ Legion::Logging.error "Failed to auto install #{extension}, e: #{e.message}"
207
+ end
208
+ return true if requested == enabled
209
+
210
+ Legion::Logging.warn "A total of #{requested - enabled} where skipped"
211
+ if ENV.key?('_') && ENV['_'].include?('bundle')
212
+ Legion::Logging.warn 'Please add them to your Gemfile since you are using bundler'
213
+ else
214
+ Legion::Logging.warn 'You must have auto_install_missing_lex set to true to auto install missing extensions'
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,47 @@
1
+ module Legion
2
+ module Extensions
3
+ module Actors
4
+ module Base
5
+ include Legion::Extensions::Helpers::Lex
6
+
7
+ def runner
8
+ Legion::Runner.run(runner_class: runner_class, function: function, check_subtask: check_subtask?, generate_task: generate_task?)
9
+ rescue StandardError => e
10
+ Legion::Logging.error e.message
11
+ Legion::Logging.error e.backtrace
12
+ end
13
+
14
+ def manual
15
+ runner_class.send(runner_function, **args)
16
+ rescue StandardError => e
17
+ Legion::Logging.error e.message
18
+ Legion::Logging.error e.backtrace
19
+ end
20
+
21
+ def function
22
+ nil
23
+ end
24
+
25
+ def use_runner?
26
+ true
27
+ end
28
+
29
+ def args
30
+ {}
31
+ end
32
+
33
+ def check_subtask?
34
+ true
35
+ end
36
+
37
+ def generate_task?
38
+ false
39
+ end
40
+
41
+ def enabled?
42
+ true
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,48 @@
1
+ require_relative 'base'
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Actors
6
+ class Every
7
+ include Legion::Extensions::Actors::Base
8
+
9
+ def initialize(**_opts)
10
+ @timer = Concurrent::TimerTask.new(execution_interval: time, timeout_interval: timeout, run_now: run_now?) do
11
+ use_runner? ? runner : manual
12
+ end
13
+
14
+ @timer.execute
15
+ rescue StandardError => e
16
+ Legion::Logging.error e.message
17
+ Legion::Logging.error e.backtrace
18
+ end
19
+
20
+ def time
21
+ 1
22
+ end
23
+
24
+ def timeout
25
+ 5
26
+ end
27
+
28
+ def run_now?
29
+ false
30
+ end
31
+
32
+ def action(**_opts)
33
+ Legion::Logging.warn 'An extension is using the default block from Legion::Extensions::Runners::Every'
34
+ end
35
+
36
+ def cancel
37
+ Legion::Logging.debug 'Cancelling Legion Timer'
38
+ return true unless @timer.respond_to?(:shutdown)
39
+
40
+ @timer.shutdown
41
+ rescue StandardError => e
42
+ Legion::Logging.error e.message
43
+ Legion::Logging.error e.backtrace
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end