toiler 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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