legionio 0.3.5 → 1.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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +45 -0
  3. data/.github/workflows/rubocop.yml +28 -0
  4. data/.github/workflows/sourcehawk-scan.yml +20 -0
  5. data/.gitignore +15 -0
  6. data/.rubocop.yml +90 -0
  7. data/CHANGELOG.md +4 -0
  8. data/CODE_OF_CONDUCT.md +75 -0
  9. data/CONTRIBUTING.md +55 -0
  10. data/Dockerfile +9 -0
  11. data/Gemfile +10 -0
  12. data/INDIVIDUAL_CONTRIBUTOR_LICENSE.md +30 -0
  13. data/LICENSE +201 -0
  14. data/NOTICE.txt +9 -0
  15. data/README.md +162 -0
  16. data/SECURITY.md +9 -0
  17. data/attribution.txt +1 -0
  18. data/docker_deploy.rb +10 -0
  19. data/exe/legion +4 -0
  20. data/exe/legionio +53 -0
  21. data/exe/lex_gen +5 -0
  22. data/legionio.gemspec +53 -0
  23. data/lib/legion.rb +24 -0
  24. data/lib/legion/cli.rb +56 -0
  25. data/lib/legion/cli/chain.rb +35 -0
  26. data/lib/legion/cli/cohort.rb +10 -0
  27. data/lib/legion/cli/function.rb +41 -0
  28. data/lib/legion/cli/lex/actor.rb +31 -0
  29. data/lib/legion/cli/lex/exchange.rb +32 -0
  30. data/lib/legion/cli/lex/message.rb +32 -0
  31. data/lib/legion/cli/lex/queue.rb +45 -0
  32. data/lib/legion/cli/lex/runner.rb +70 -0
  33. data/lib/legion/cli/lex/templates/actor.erb +6 -0
  34. data/lib/legion/cli/lex/templates/actor_spec.erb +0 -0
  35. data/lib/legion/cli/lex/templates/base/bitbucket.yml.erb +69 -0
  36. data/lib/legion/cli/lex/templates/base/gemfile.erb +3 -0
  37. data/lib/legion/cli/lex/templates/base/gemspec.erb +26 -0
  38. data/lib/legion/cli/lex/templates/base/gitignore.erb +11 -0
  39. data/lib/legion/cli/lex/templates/base/lex.erb +9 -0
  40. data/lib/legion/cli/lex/templates/base/lex_spec.erb +5 -0
  41. data/lib/legion/cli/lex/templates/base/lic.erb +21 -0
  42. data/lib/legion/cli/lex/templates/base/rakefile.erb +6 -0
  43. data/lib/legion/cli/lex/templates/base/readme.md.erb +2 -0
  44. data/lib/legion/cli/lex/templates/base/rubocop.yml.erb +15 -0
  45. data/lib/legion/cli/lex/templates/base/spec_helper.rb.erb +11 -0
  46. data/lib/legion/cli/lex/templates/base/version.erb +7 -0
  47. data/lib/legion/cli/lex/templates/exchange.erb +11 -0
  48. data/lib/legion/cli/lex/templates/exchange_spec.erb +0 -0
  49. data/lib/legion/cli/lex/templates/message.erb +23 -0
  50. data/lib/legion/cli/lex/templates/message_spec.erb +0 -0
  51. data/lib/legion/cli/lex/templates/queue.erb +12 -0
  52. data/lib/legion/cli/lex/templates/queue_helper.erb +24 -0
  53. data/lib/legion/cli/lex/templates/queue_spec.erb +11 -0
  54. data/lib/legion/cli/lex/templates/runner.erb +11 -0
  55. data/lib/legion/cli/lex/templates/runner_spec.erb +11 -0
  56. data/lib/legion/cli/relationship.rb +22 -0
  57. data/lib/legion/cli/task.rb +49 -0
  58. data/lib/legion/cli/trigger.rb +88 -0
  59. data/lib/legion/cli/version.rb +5 -0
  60. data/lib/legion/extensions.rb +229 -0
  61. data/lib/legion/extensions/actors/base.rb +47 -0
  62. data/lib/legion/extensions/actors/defaults.rb +28 -0
  63. data/lib/legion/extensions/actors/every.rb +48 -0
  64. data/lib/legion/extensions/actors/loop.rb +32 -0
  65. data/lib/legion/extensions/actors/nothing.rb +15 -0
  66. data/lib/legion/extensions/actors/once.rb +40 -0
  67. data/lib/legion/extensions/actors/poll.rb +87 -0
  68. data/lib/legion/extensions/actors/subscription.rb +139 -0
  69. data/lib/legion/extensions/builders/actors.rb +61 -0
  70. data/lib/legion/extensions/builders/base.rb +36 -0
  71. data/lib/legion/extensions/builders/helpers.rb +24 -0
  72. data/lib/legion/extensions/builders/runners.rb +62 -0
  73. data/lib/legion/extensions/core.rb +131 -0
  74. data/lib/legion/extensions/data.rb +61 -0
  75. data/lib/legion/extensions/data/migrator.rb +33 -0
  76. data/lib/legion/extensions/data/model.rb +8 -0
  77. data/lib/legion/extensions/helpers/base.rb +82 -0
  78. data/lib/legion/extensions/helpers/cache.rb +23 -0
  79. data/lib/legion/extensions/helpers/core.rb +41 -0
  80. data/lib/legion/extensions/helpers/data.rb +23 -0
  81. data/lib/legion/extensions/helpers/lex.rb +48 -0
  82. data/lib/legion/extensions/helpers/logger.rb +44 -0
  83. data/lib/legion/extensions/helpers/task.rb +60 -0
  84. data/lib/legion/extensions/helpers/transport.rb +44 -0
  85. data/lib/legion/extensions/transport.rb +159 -0
  86. data/lib/legion/lex.rb +89 -0
  87. data/lib/legion/process.rb +124 -0
  88. data/lib/legion/runner.rb +57 -0
  89. data/lib/legion/runner/log.rb +10 -0
  90. data/lib/legion/runner/status.rb +69 -0
  91. data/lib/legion/service.rb +130 -0
  92. data/lib/legion/supervision.rb +15 -0
  93. data/lib/legion/version.rb +3 -0
  94. data/sourcehawk.yml +4 -0
  95. metadata +142 -168
data/lib/legion/lex.rb ADDED
@@ -0,0 +1,89 @@
1
+ require 'thor'
2
+ require 'legion/cli/version'
3
+ require 'legion/cli/lex/actor'
4
+ require 'legion/cli/lex/exchange'
5
+ require 'legion/cli/lex/message'
6
+ require 'legion/cli/lex/queue'
7
+ require 'legion/cli/lex/runner'
8
+
9
+ module Legion
10
+ class Cli
11
+ class LexBuilder < Thor
12
+ check_unknown_options!
13
+ include Thor::Actions
14
+
15
+ no_commands do
16
+ def lex
17
+ Dir.pwd.split('/').last.split('-').last
18
+ end
19
+ end
20
+
21
+ def self.exit_on_failure?
22
+ true
23
+ end
24
+
25
+ def self.source_root
26
+ File.dirname(__FILE__)
27
+ end
28
+
29
+ desc 'actor', 'creates and manages actors'
30
+ subcommand 'actor', Legion::Cli::Lex::Actor
31
+
32
+ desc 'exchange', 'creates and manages exchanges'
33
+ subcommand 'exchange', Legion::Cli::Lex::Exchange
34
+
35
+ desc 'messages', 'creates and manages messages'
36
+ subcommand 'message', Legion::Cli::Lex::Message
37
+
38
+ desc 'queue', 'creates and manages queues'
39
+ subcommand 'queue', Legion::Cli::Lex::Queue
40
+
41
+ desc 'runner', 'creates and manages runners'
42
+ subcommand 'runner', Legion::Cli::Lex::Runner
43
+
44
+ desc 'version', 'Display Version'
45
+ map %w[-v --version] => :version
46
+ def version
47
+ say "Legion::CLI #{Legion::Cli::VERSION}"
48
+ end
49
+
50
+ method_option rspec: true
51
+ method_option pipeline: true
52
+ method_option git_init: true
53
+ method_option bundle_install: true
54
+ desc 'create :name', 'creates a new lex'
55
+ def create(name)
56
+ if Dir.pwd.include?('lex-')
57
+ say('already inside a lex_gen, try moving to a different directory', :red)
58
+ return nil
59
+ end
60
+
61
+ vars = { filename: "lex-#{name}", class_name: name.capitalize, lex: name }
62
+ filename = vars[:filename]
63
+ template('cli/lex/templates/base/gemspec.erb', "#{filename}/#{filename}.gemspec", vars)
64
+ template('cli/lex/templates/base/gemfile.erb', "#{filename}/Gemfile", vars)
65
+ template('cli/lex/templates/base/gitignore.erb', "#{filename}/.gitignore", vars)
66
+ template('cli/lex/templates/base/lic.erb', "#{filename}/LICENSE.txt", vars)
67
+ template('cli/lex/templates/base/rakefile.erb', "#{filename}/Rakefile", vars)
68
+ template('cli/lex/templates/base/rubocop.yml.erb', "#{filename}/.rubocop.yml", vars)
69
+ template('cli/lex/templates/base/readme.md.erb', "#{filename}/README.md", **vars)
70
+ template('cli/lex/templates/base/lex.erb', "#{filename}/lib/legion/extensions/#{name}.rb", vars)
71
+ template('cli/lex/templates/base/version.erb', "#{filename}/lib/legion/extensions/#{name}/version.rb", vars)
72
+ template('cli/lex/templates/base/bitbucket.yml.erb', "#{filename}/bitbucket-pipelines.yml", vars) if options[:pipeline]
73
+ template('cli/lex/templates/base/spec_helper.rb.erb', "#{filename}/spec/spec_helper.rb", vars)
74
+ template('cli/lex/templates/base/lex_spec.erb', "#{filename}/spec/legion/#{name}_spec.rb", vars)
75
+
76
+ return if !options[:git_init] && !options[:bundle_install]
77
+
78
+ run("cd lex_gen-#{filename}")
79
+ if options[:git_init]
80
+ run('git init')
81
+ run('git add .')
82
+ run('git commit -m \'Initial commit\'')
83
+ end
84
+
85
+ run('bundle update') if options[:bundle_install]
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,124 @@
1
+ require 'fileutils'
2
+
3
+ module Legion
4
+ class Process
5
+ def self.run!(options)
6
+ Legion::Process.new(options).run!
7
+ end
8
+
9
+ attr_reader :options, :quit, :service
10
+
11
+ def initialize(options)
12
+ @options = options
13
+ options[:logfile] = File.expand_path(logfile) if logfile?
14
+ options[:pidfile] = File.expand_path(pidfile) if pidfile?
15
+ end
16
+
17
+ def daemonize?
18
+ options[:daemonize]
19
+ end
20
+
21
+ def logfile
22
+ options[:logfile]
23
+ end
24
+
25
+ def pidfile
26
+ options[:pidfile]
27
+ end
28
+
29
+ def logfile?
30
+ !logfile.nil?
31
+ end
32
+
33
+ def pidfile?
34
+ !pidfile.nil?
35
+ end
36
+
37
+ def info(msg)
38
+ puts "[#{::Process.pid}] [#{Time.now}] #{msg}"
39
+ end
40
+
41
+ def run!
42
+ start_time = Time.now
43
+ @options[:time_limit] = @options[:time_limit].to_i if @options.key? :time_limit
44
+ @quit = false
45
+ check_pid
46
+ daemonize if daemonize?
47
+ write_pid
48
+ trap_signals
49
+
50
+ until quit
51
+ sleep(1)
52
+ @quit = true if @options.key?(:time_limit) && Time.now - start_time > @options[:time_limit]
53
+ end
54
+ Legion::Logging.info('Legion is shutting down!')
55
+ Legion.shutdown
56
+ Legion::Logging.info('Legion has shutdown. Goodbye!')
57
+
58
+ exit
59
+ end
60
+
61
+ #==========================================================================
62
+ # DAEMONIZING, PID MANAGEMENT, and OUTPUT REDIRECTION
63
+ #==========================================================================
64
+
65
+ def daemonize
66
+ exit if fork
67
+ ::Process.setsid
68
+ exit if fork
69
+ Dir.chdir '/'
70
+ end
71
+
72
+ def write_pid
73
+ if pidfile?
74
+ begin
75
+ File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) { |f| f.write(::Process.pid.to_s) }
76
+ at_exit { File.delete(pidfile) if File.exist?(pidfile) }
77
+ rescue Errno::EEXIST
78
+ check_pid
79
+ retry
80
+ end
81
+ end
82
+ false
83
+ end
84
+
85
+ def check_pid
86
+ if pidfile?
87
+ case pid_status(pidfile)
88
+ when :running, :not_owned
89
+ exit(1)
90
+ when :dead
91
+ File.delete(pidfile)
92
+ end
93
+ end
94
+ false
95
+ end
96
+
97
+ def pid_status(pidfile)
98
+ return :exited unless File.exist?(pidfile)
99
+
100
+ pid = ::File.read(pidfile).to_i
101
+ return :dead if pid.zero?
102
+
103
+ ::Process.kill(0, pid)
104
+ :running
105
+ rescue Errno::ESRCH
106
+ :dead
107
+ rescue Errno::EPERM
108
+ :not_owned
109
+ end
110
+
111
+ def trap_signals
112
+ trap('SIGTERM') do
113
+ info 'sigterm'
114
+ end
115
+
116
+ trap('SIGHUP') do
117
+ info 'sithup'
118
+ end
119
+ trap('SIGINT') do
120
+ @quit = true
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,57 @@
1
+ require_relative 'runner/log'
2
+ require_relative 'runner/status'
3
+ require 'legion/transport'
4
+ require 'legion/transport/messages/check_subtask'
5
+
6
+ module Legion
7
+ module Runner
8
+ def self.run(runner_class:, function:, task_id: nil, args: nil, check_subtask: true, generate_task: true, parent_id: nil, master_id: nil, catch_exceptions: false, **opts) # rubocop:disable Layout/LineLength, Metrics/CyclomaticComplexity, Metrics/ParameterLists
9
+ runner_class = Kernel.const_get(runner_class) if runner_class.is_a? String
10
+
11
+ if task_id.nil? && generate_task
12
+ task_gen = Legion::Runner::Status.generate_task_id(
13
+ function: function,
14
+ runner_class: runner_class,
15
+ parent_id: parent_id, master_id: master_id, task_id: task_id, **opts
16
+ )
17
+ task_id = task_gen[:task_id] unless task_gen.nil?
18
+ end
19
+
20
+ args = opts if args.nil?
21
+ args[:task_id] = task_id unless task_id.nil?
22
+ args[:master_id] = master_id unless master_id.nil?
23
+ args[:parent_id] = parent_id unless parent_id.nil?
24
+
25
+ # result = Fiber.new { Fiber.yield runner_class.send(function, **args) }
26
+ raise 'No Function defined' if function.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
+ raise e unless catch_exceptions
44
+ ensure
45
+ status = 'task.completed' if status.nil?
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,10 @@
1
+ module Legion
2
+ module Runner
3
+ module Log
4
+ def self.exception(exc, **opts)
5
+ Legion::Logging.error exc.message
6
+ Legion::Logging.error opts
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,69 @@
1
+ module Legion
2
+ module Runner
3
+ module Status
4
+ def self.update(task_id:, status: 'task.completed', **opts)
5
+ Legion::Logging.debug "Legion::Runner::Status.update called, #{task_id}, status: #{status}, #{opts}"
6
+ return if status.nil?
7
+
8
+ if Legion::Settings[:data][:connected]
9
+ update_db(task_id: task_id, status: status, **opts)
10
+ else
11
+ update_rmq(task_id: task_id, status: status, **opts)
12
+ end
13
+ end
14
+
15
+ def self.update_rmq(task_id:, status: 'task.completed', **opts)
16
+ return if status.nil?
17
+
18
+ Legion::Transport::Messages::TaskUpdate.new(task_id: task_id, status: status, **opts).publish
19
+ rescue StandardError => e
20
+ Legion::Logging.fatal e.message
21
+ Legion::Logging.fatal e.backtrace
22
+ retries ||= 0
23
+ Legion::Logging.fatal 'Will retry in 3 seconds' if retries < 5
24
+ sleep(3)
25
+ retry if (retries += 1) < 5
26
+ end
27
+
28
+ def self.update_db(task_id:, status: 'task.completed', **opts)
29
+ return if status.nil?
30
+
31
+ task = Legion::Data::Model::Task[task_id]
32
+ task.update(status: status)
33
+ rescue StandardError => e
34
+ Legion::Logging.warn e.message
35
+ Legion::Logging.warn 'Legion::Runner.update_status_db failed, defaulting to rabbitmq'
36
+ Legion::Logging.warn e.backtrace
37
+ update_rmq(task_id: task_id, status: status, **opts)
38
+ end
39
+
40
+ def self.generate_task_id(runner_class:, function:, status: 'task.queued', **opts)
41
+ Legion::Logging.debug "Legion::Runner::Status.generate_task_id called, #{runner_class}, #{function}, status: #{status}, #{opts}"
42
+ return nil unless Legion::Settings[:data][:connected]
43
+
44
+ runner = Legion::Data::Model::Runner.where(namespace: runner_class.to_s.downcase).first
45
+ return nil if runner.nil?
46
+
47
+ function = Legion::Data::Model::Function.where(runner_id: runner.values[:id], name: function).first
48
+ return nil if function.nil?
49
+
50
+ insert = { status: status, function_id: function.values[:id] }
51
+ insert[:parent_id] = opts[:task_id] if opts.key? :task_id
52
+ insert[:master_id] = opts[:task_id] if opts.key? :task_id
53
+ insert[:payload] = Legion::JSON.dump(opts[:payload]) if opts.key? :payload
54
+
55
+ %i[function_args master_id parent_id relationship_id].each do |column|
56
+ next unless opts.key? column
57
+
58
+ insert[column] = opts[column].is_a?(Hash) ? Legion::JSON.dump(opts[column]) : opts[column]
59
+ end
60
+
61
+ { success: true, task_id: Legion::Data::Model::Task.insert(insert), **insert }
62
+ rescue StandardError => e
63
+ Legion::Logging.error e.message
64
+ Legion::Logging.error e.backtrace
65
+ raise(e)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,130 @@
1
+ module Legion
2
+ class Service
3
+ def modules
4
+ [Legion::Crypt, Legion::Transport, Legion::Cache, Legion::Data, Legion::Supervision].freeze
5
+ end
6
+
7
+ def initialize(transport: true, cache: true, data: true, supervision: true, extensions: true, crypt: true, log_level: 'info') # rubocop:disable Metrics/ParameterLists
8
+ setup_logging(log_level: log_level)
9
+ Legion::Logging.debug('Starting Legion::Service')
10
+ setup_settings
11
+ Legion::Logging.info("node name: #{Legion::Settings[:client][:name]}")
12
+
13
+ if crypt
14
+ require 'legion/crypt'
15
+ Legion::Crypt.start
16
+ end
17
+
18
+ setup_transport if transport
19
+
20
+ require 'legion/cache' if cache
21
+
22
+ setup_data if data
23
+ setup_supervision if supervision
24
+ require 'legion/runner'
25
+ load_extensions if extensions
26
+
27
+ Legion::Crypt.cs if crypt
28
+ Legion::Settings[:client][:ready] = true
29
+ end
30
+
31
+ def setup_data
32
+ if RUBY_ENGINE == 'truffleruby'
33
+ Legion::Logging.error 'Legion::Data does not support truffleruby, please use MRI for any LEX that require it '
34
+ Legion::Settings[:data][:connected] = false
35
+ return false
36
+ end
37
+
38
+ require 'legion/data'
39
+ Legion::Settings.merge_settings(:data, Legion::Data::Settings.default)
40
+ Legion::Data.setup
41
+ rescue LoadError
42
+ Legion::Logging.info 'Legion::Data gem is not installed, please install it manually with gem install legion-data'
43
+ rescue StandardError => e
44
+ Legion::Logging.warn "Legion::Data failed to load, starting without it. e: #{e.message}"
45
+ end
46
+
47
+ # noinspection RubyArgCount
48
+ def default_paths
49
+ [
50
+ '/etc/legionio',
51
+ "#{ENV['home']}/legionio",
52
+ '~/legionio',
53
+ './settings'
54
+ ]
55
+ end
56
+
57
+ def setup_settings(default_dir = __dir__)
58
+ require 'legion/settings'
59
+ config_directory = default_dir
60
+ default_paths.each do |path|
61
+ next unless Dir.exist? path
62
+
63
+ Legion::Logging.info "Using #{path} for settings"
64
+ config_directory = path
65
+ break
66
+ end
67
+
68
+ Legion::Logging.info "Using directory #{config_directory} for settings"
69
+ Legion::Settings.load(config_dir: config_directory)
70
+ Legion::Logging.info('Legion::Settings Loaded')
71
+ end
72
+
73
+ def setup_logging(log_level: 'info', **_opts)
74
+ require 'legion/logging'
75
+ Legion::Logging.setup(log_level: log_level, level: log_level, trace: true)
76
+ end
77
+
78
+ def setup_transport
79
+ require 'legion/transport'
80
+ Legion::Settings.merge_settings('transport', Legion::Transport::Settings.default)
81
+ Legion::Transport::Connection.setup
82
+ end
83
+
84
+ def setup_supervision
85
+ require 'legion/supervision'
86
+ @supervision = Legion::Supervision.setup
87
+ end
88
+
89
+ def shutdown
90
+ Legion::Logging.info('Legion::Service.shutdown was called')
91
+ @shutdown = true
92
+ Legion::Settings[:client][:shutting_down] = true
93
+ sleep(0.5)
94
+ Legion::Extensions.shutdown
95
+ sleep(1)
96
+ Legion::Data.shutdown if Legion::Settings[:data][:connected]
97
+ Legion::Cache.shutdown
98
+ Legion::Transport::Connection.shutdown
99
+ Legion::Crypt.shutdown
100
+ end
101
+
102
+ def reload
103
+ Legion::Logging.info 'Legion::Service.reload was called'
104
+ Legion::Extensions.shutdown
105
+ sleep(1)
106
+ Legion::Data.shutdown
107
+ Legion::Cache.shutdown
108
+ Legion::Transport::Connection.shutdown
109
+ Legion::Crypt.shutdown
110
+ Legion::Settings[:client][:ready] = false
111
+
112
+ sleep(5)
113
+ setup_settings
114
+ Legion::Crypt.start
115
+ setup_transport
116
+ setup_data
117
+ setup_supervision
118
+ load_extensions
119
+
120
+ Legion::Crypt.cs
121
+ Legion::Settings[:client][:ready] = true
122
+ Legion::Logging.info 'Legion has been reloaded'
123
+ end
124
+
125
+ def load_extensions
126
+ require 'legion/runner'
127
+ Legion::Extensions.hook_extensions
128
+ end
129
+ end
130
+ end