toiler 0.1.5 → 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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/Gemfile +7 -4
  4. data/Gemfile.lock +26 -57
  5. data/README.md +15 -0
  6. data/lib/toiler.rb +15 -44
  7. data/lib/toiler/actor/fetcher.rb +89 -0
  8. data/lib/toiler/actor/processor.rb +106 -0
  9. data/lib/toiler/actor/supervisor.rb +45 -0
  10. data/lib/toiler/actor/utils/actor_logging.rb +28 -0
  11. data/lib/toiler/aws/message.rb +64 -0
  12. data/lib/toiler/aws/queue.rb +61 -0
  13. data/lib/toiler/cli.rb +67 -94
  14. data/lib/toiler/utils/argument_parser.rb +50 -0
  15. data/lib/toiler/utils/environment_loader.rb +104 -0
  16. data/lib/toiler/utils/logging.rb +37 -0
  17. data/lib/toiler/version.rb +2 -1
  18. data/lib/toiler/worker.rb +35 -15
  19. data/toiler.gemspec +4 -4
  20. metadata +32 -32
  21. data/celluloid-task-pooledfiber/.gitignore +0 -9
  22. data/celluloid-task-pooledfiber/.rspec +0 -2
  23. data/celluloid-task-pooledfiber/.travis.yml +0 -5
  24. data/celluloid-task-pooledfiber/Gemfile +0 -11
  25. data/celluloid-task-pooledfiber/LICENSE.txt +0 -21
  26. data/celluloid-task-pooledfiber/README.md +0 -37
  27. data/celluloid-task-pooledfiber/Rakefile +0 -8
  28. data/celluloid-task-pooledfiber/celluloid-task-pooled-fiber.gemspec +0 -18
  29. data/celluloid-task-pooledfiber/lib/celluloid/task/pooled_fiber.rb +0 -26
  30. data/celluloid-task-pooledfiber/lib/celluloid/util/fiber_pool.rb +0 -95
  31. data/celluloid-task-pooledfiber/spec/celluloid/tasks/pooled_fiber_spec.rb +0 -5
  32. data/celluloid-task-pooledfiber/spec/spec_helper.rb +0 -60
  33. data/celluloid-task-pooledfiber/spec/support/shared_examples_for_task.rb +0 -49
  34. data/lib/toiler/core_ext.rb +0 -47
  35. data/lib/toiler/environment_loader.rb +0 -82
  36. data/lib/toiler/fetcher.rb +0 -56
  37. data/lib/toiler/logging.rb +0 -42
  38. data/lib/toiler/manager.rb +0 -71
  39. data/lib/toiler/message.rb +0 -60
  40. data/lib/toiler/processor.rb +0 -86
  41. data/lib/toiler/queue.rb +0 -53
  42. data/lib/toiler/scheduler.rb +0 -16
  43. data/lib/toiler/supervisor.rb +0 -66
@@ -0,0 +1,45 @@
1
+ require 'toiler/actor/fetcher'
2
+ require 'toiler/actor/processor'
3
+
4
+ module Toiler
5
+ module Actor
6
+ # Actor that starts and supervises Toiler's actors
7
+ class Supervisor < Concurrent::Actor::RestartingContext
8
+ attr_accessor :client
9
+
10
+ def initialize
11
+ @client = ::Aws::SQS::Client.new
12
+ spawn_fetchers
13
+ spawn_processors
14
+ end
15
+
16
+ def on_message(_msg)
17
+ pass
18
+ end
19
+
20
+ def queues
21
+ Toiler.worker_class_registry
22
+ end
23
+
24
+ def spawn_fetchers
25
+ queues.each do |queue, _klass|
26
+ fetcher = Actor::Fetcher.spawn! name: "fetcher_#{queue}".to_sym,
27
+ supervise: true, args: [queue, client]
28
+ Toiler.set_fetcher queue, fetcher
29
+ end
30
+ end
31
+
32
+ def spawn_processors
33
+ queues.each do |queue, klass|
34
+ name = "processor_pool_#{queue}".to_sym
35
+ count = klass.concurrency
36
+ pool = Concurrent::Actor::Utils::Pool.spawn! name, count do |index|
37
+ Actor::Processor.spawn name: "processor_#{queue}_#{index}".to_sym,
38
+ supervise: true, args: [queue]
39
+ end
40
+ Toiler.set_processor_pool queue, pool
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ module Toiler
2
+ module Actor
3
+ module Utils
4
+ # Provides helper methods for logging
5
+ module ActorLogging
6
+ def error(msg)
7
+ log Logger::Severity::ERROR, self.class, msg
8
+ end
9
+
10
+ def info(msg)
11
+ log Logger::Severity::INFO, self.class, msg
12
+ end
13
+
14
+ def debug(msg)
15
+ log Logger::Severity::DEBUG, self.class, msg
16
+ end
17
+
18
+ def warn(msg)
19
+ log Logger::Severity::WARN, self.class, msg
20
+ end
21
+
22
+ def fatal(msg)
23
+ log Logger::Severity::FATAL, self.class, msg
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,64 @@
1
+ module Toiler
2
+ module Aws
3
+ # SQS Message abstraction
4
+ # Provides methods for querying and acting on a SQS message
5
+ class Message
6
+ attr_accessor :client, :queue_url, :data
7
+
8
+ def initialize(client, queue_url, data)
9
+ @client = client
10
+ @queue_url = queue_url
11
+ @data = data
12
+ end
13
+
14
+ def delete
15
+ client.delete_message(
16
+ queue_url: queue_url,
17
+ receipt_handle: data.receipt_handle
18
+ )
19
+ end
20
+
21
+ def change_visibility(options)
22
+ client.change_message_visibility(
23
+ options.merge(queue_url: queue_url, receipt_handle: receipt_handle)
24
+ )
25
+ end
26
+
27
+ def visibility_timeout=(timeout)
28
+ client.change_message_visibility(
29
+ queue_url: queue_url,
30
+ receipt_handle: data.receipt_handle,
31
+ visibility_timeout: timeout
32
+ )
33
+ end
34
+
35
+ def message_id
36
+ data.message_id
37
+ end
38
+
39
+ def receipt_handle
40
+ data.receipt_handle
41
+ end
42
+
43
+ def md5_of_body
44
+ data.md5_of_body
45
+ end
46
+
47
+ def body
48
+ data.body
49
+ end
50
+
51
+ def attributes
52
+ data.attributes
53
+ end
54
+
55
+ def md5_of_message_attributes
56
+ data.md5_of_message_attributes
57
+ end
58
+
59
+ def message_attributes
60
+ data.message_attributes
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,61 @@
1
+ require 'toiler/aws/message'
2
+
3
+ module Toiler
4
+ module Aws
5
+ # SQS Queue abstraction
6
+ # Provides methods for querying and acting on a SQS queue
7
+ class Queue
8
+ attr_accessor :name, :client, :url
9
+
10
+ def initialize(name, client = nil)
11
+ @name = name
12
+ @client = client || ::Aws::SQS::Client.new
13
+ @url = client.get_queue_url(queue_name: name).queue_url
14
+ end
15
+
16
+ def visibility_timeout
17
+ client.get_queue_attributes(
18
+ queue_url: url,
19
+ attribute_names: ['VisibilityTimeout']
20
+ ).attributes['VisibilityTimeout'].to_i
21
+ end
22
+
23
+ def delete_messages(options)
24
+ client.delete_message_batch options.merge queue_url: url
25
+ end
26
+
27
+ def send_message(options)
28
+ client.send_message sanitize_message_body options.merge queue_url: url
29
+ end
30
+
31
+ def send_messages(options)
32
+ client.send_message_batch(
33
+ sanitize_message_body options.merge queue_url: url
34
+ )
35
+ end
36
+
37
+ def receive_messages(options)
38
+ client.receive_message(options.merge(queue_url: url))
39
+ .messages
40
+ .map { |m| Message.new(client, url, m) }
41
+ end
42
+
43
+ private
44
+
45
+ def sanitize_message_body(options)
46
+ messages = options[:entries] || [options]
47
+
48
+ messages.each do |m|
49
+ body = m[:message_body]
50
+ if body.is_a?(Hash)
51
+ m[:message_body] = JSON.dump(body)
52
+ elsif !body.is_a? String
53
+ fail ArgumentError, "Body must be a String, found #{body.class}"
54
+ end
55
+ end
56
+
57
+ options
58
+ end
59
+ end
60
+ end
61
+ end
data/lib/toiler/cli.rb CHANGED
@@ -7,71 +7,81 @@ module Toiler
7
7
  # See: https://github.com/mperham/sidekiq/blob/33f5d6b2b6c0dfaab11e5d39688cab7ebadc83ae/lib/sidekiq/cli.rb#L20
8
8
  class Shutdown < Interrupt; end
9
9
 
10
+ # Command line client interface
10
11
  class CLI
11
12
  include Singleton
12
13
 
13
- def run(args)
14
- self_read, self_write = IO.pipe
15
-
16
- %w(INT TERM USR1 USR2 TTIN).each do |sig|
17
- begin
18
- trap sig do
19
- self_write.puts(sig)
20
- end
21
- rescue ArgumentError
22
- puts "Signal #{sig} not supported"
23
- end
24
- end
14
+ attr_accessor :supervisor
25
15
 
26
- options = parse_cli_args(args)
16
+ def run(args)
17
+ @self_read, @self_write = IO.pipe
27
18
 
28
- EnvironmentLoader.load(options)
19
+ trap_signals
20
+ options = Utils::ArgumentParser.parse(args)
21
+ Utils::EnvironmentLoader.load(options)
29
22
  daemonize
30
23
  write_pid
31
- load_celluloid
24
+ load_concurrent
25
+ start_supervisor
32
26
 
33
- begin
34
- require 'toiler/supervisor'
35
- @supervisor = Supervisor.new
27
+ handle_stop
28
+ end
36
29
 
37
- while (readable_io = IO.select([self_read]))
38
- signal = readable_io.first[0].gets.strip
39
- handle_signal(signal)
40
- end
41
- rescue Interrupt
42
- puts 'Received interrupt, terminating actors...'
30
+ private
31
+
32
+ def handle_stop
33
+ while (readable_io = IO.select([@self_read]))
34
+ handle_signal(readable_io.first[0].gets.strip)
35
+ end
36
+ rescue Interrupt
37
+ puts 'Waiting up to 60 seconds for actors to finish...'
38
+ supervisor.ask(:terminate!).wait(60)
39
+ ensure
40
+ exit 0
41
+ end
42
+
43
+ def shutdown_pools
44
+ Concurrent.global_fast_executor.shutdown
45
+ Concurrent.global_io_executor.shutdown
46
+ return if Concurrent.global_io_executor.wait_for_termination(60)
47
+ Concurrent.global_io_executor.kill
48
+ end
49
+
50
+ def start_supervisor
51
+ require 'toiler/actor/supervisor'
52
+ @supervisor = Actor::Supervisor.spawn! :supervisor
53
+ end
54
+
55
+ def trap_signals
56
+ %w(INT TERM USR1 USR2 TTIN).each do |sig|
43
57
  begin
44
- Timeout.timeout(20) do
45
- @supervisor.stop
58
+ trap sig do
59
+ @self_write.puts(sig)
46
60
  end
47
- ensure
48
- exit 0
61
+ rescue ArgumentError
62
+ puts "System does not support signal #{sig}"
49
63
  end
50
64
  end
51
65
  end
52
66
 
53
- private
54
-
55
- def handle_signal(_signal)
56
- fail Interrupt
67
+ def handle_signal(signal)
68
+ case signal
69
+ when 'INT', 'TERM'
70
+ fail Interrupt
71
+ end
57
72
  end
58
73
 
59
- def load_celluloid
60
- fail "Celluloid cannot be required until here, or it will break Toiler's daemonization" if defined?(::Celluloid) && Toiler.options[:daemon]
61
-
62
- # Celluloid can't be loaded until after we've daemonized
63
- # because it spins up threads and creates locks which get
64
- # into a very bad state if forked.
65
- require 'celluloid/current'
66
- require 'celluloid/task/pooled_fiber'
67
- Celluloid.task_class = Celluloid::Task::PooledFiber
68
- Celluloid.logger = (Toiler.options[:verbose] ? Toiler.logger : nil)
74
+ def load_concurrent
75
+ fail 'Concurrent should not be required now' if defined?(::Concurrent)
76
+ require 'concurrent-edge'
77
+ Concurrent.global_logger = lambda do |level, progname, msg = nil, &block|
78
+ Toiler.logger.log(level, msg, progname, &block)
79
+ end if Toiler.logger
69
80
  end
70
81
 
71
82
  def daemonize
72
83
  return unless Toiler.options[:daemon]
73
-
74
- fail ArgumentError, "You really should set a logfile if you're going to daemonize" unless Toiler.options[:logfile]
84
+ fail 'Logfile required when daemonizing' unless Toiler.options[:logfile]
75
85
 
76
86
  files_to_reopen = []
77
87
  ObjectSpace.each_object(File) do |file|
@@ -80,71 +90,34 @@ module Toiler
80
90
 
81
91
  Process.daemon(true, true)
82
92
 
93
+ reopen_files(files_to_reopen)
94
+ reopen_std
95
+ end
96
+
97
+ def reopen_files(files_to_reopen)
83
98
  files_to_reopen.each do |file|
84
99
  begin
85
100
  file.reopen file.path, 'a+'
86
- # file.sync = true
87
- rescue ::Exception
101
+ file.sync = true
102
+ rescue StandardError
103
+ puts "Failed to reopen file #{file}"
88
104
  end
89
105
  end
106
+ end
90
107
 
108
+ def reopen_std
91
109
  [$stdout, $stderr].each do |io|
92
110
  File.open(Toiler.options[:logfile], 'ab') do |f|
93
111
  io.reopen(f)
94
112
  end
95
- # io.sync = true
113
+ io.sync = true
96
114
  end
97
115
  $stdin.reopen('/dev/null')
98
116
  end
99
117
 
100
118
  def write_pid
101
- if (path = Toiler.options[:pidfile])
102
- File.open(path, 'w') do |f|
103
- f.puts Process.pid
104
- end
105
- end
106
- end
107
-
108
- def parse_cli_args(argv)
109
- opts = { queues: [] }
110
-
111
- @parser = OptionParser.new do |o|
112
- o.on '-d', '--daemon', 'Daemonize process' do |arg|
113
- opts[:daemon] = arg
114
- end
115
-
116
- o.on '-r', '--require [PATH|DIR]', 'Location of the worker' do |arg|
117
- opts[:require] = arg
118
- end
119
-
120
- o.on '-C', '--config PATH', 'Path to YAML config file' do |arg|
121
- opts[:config_file] = arg
122
- end
123
-
124
- o.on '-R', '--rails', 'Load Rails' do |arg|
125
- opts[:rails] = arg
126
- end
127
-
128
- o.on '-L', '--logfile PATH', 'Path to writable logfile' do |arg|
129
- opts[:logfile] = arg
130
- end
131
-
132
- o.on '-P', '--pidfile PATH', 'Path to pidfile' do |arg|
133
- opts[:pidfile] = arg
134
- end
135
-
136
- o.on '-v', '--verbose', 'Print more verbose output' do |arg|
137
- opts[:verbose] = arg
138
- end
139
- end
140
-
141
- @parser.banner = 'toiler [options]'
142
- @parser.on_tail '-h', '--help', 'Show help' do
143
- Toiler.logger.info @parser
144
- exit 1
145
- end
146
- @parser.parse!(argv)
147
- opts
119
+ file = Toiler.options[:pidfile]
120
+ File.write file, Process.pid if file
148
121
  end
149
122
  end
150
123
  end
@@ -0,0 +1,50 @@
1
+ module Toiler
2
+ module Utils
3
+ # Parses command-line arguments
4
+ module ArgumentParser
5
+ module_function
6
+
7
+ def parse(argv)
8
+ opts = { queues: [] }
9
+
10
+ parser = OptionParser.new do |o|
11
+ o.on '-d', '--daemon', 'Daemonize process' do |arg|
12
+ opts[:daemon] = arg
13
+ end
14
+
15
+ o.on '-r', '--require [PATH|DIR]', 'Location of the worker' do |arg|
16
+ opts[:require] = arg
17
+ end
18
+
19
+ o.on '-C', '--config PATH', 'Path to YAML config file' do |arg|
20
+ opts[:config_file] = arg
21
+ end
22
+
23
+ o.on '-R', '--rails', 'Load Rails' do |arg|
24
+ opts[:rails] = arg
25
+ end
26
+
27
+ o.on '-L', '--logfile PATH', 'Path to writable logfile' do |arg|
28
+ opts[:logfile] = arg
29
+ end
30
+
31
+ o.on '-P', '--pidfile PATH', 'Path to pidfile' do |arg|
32
+ opts[:pidfile] = arg
33
+ end
34
+
35
+ o.on '-v', '--verbose', 'Print more verbose output' do |arg|
36
+ opts[:verbose] = arg
37
+ end
38
+ end
39
+
40
+ parser.banner = 'toiler [options]'
41
+ parser.on_tail '-h', '--help', 'Show help' do
42
+ puts parser
43
+ exit 1
44
+ end
45
+ parser.parse!(argv)
46
+ opts
47
+ end
48
+ end
49
+ end
50
+ end