emque-consuming 1.0.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +40 -0
  3. data/.travis.yml +10 -0
  4. data/CHANGELOG.md +11 -0
  5. data/Gemfile +7 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +205 -0
  8. data/Rakefile +14 -0
  9. data/bin/emque +5 -0
  10. data/emque-consuming.gemspec +36 -0
  11. data/lib/emque/consuming/actor.rb +21 -0
  12. data/lib/emque/consuming/adapter.rb +47 -0
  13. data/lib/emque/consuming/adapters/rabbit_mq/manager.rb +111 -0
  14. data/lib/emque/consuming/adapters/rabbit_mq/retry_worker.rb +59 -0
  15. data/lib/emque/consuming/adapters/rabbit_mq/worker.rb +87 -0
  16. data/lib/emque/consuming/adapters/rabbit_mq.rb +26 -0
  17. data/lib/emque/consuming/application.rb +112 -0
  18. data/lib/emque/consuming/cli.rb +140 -0
  19. data/lib/emque/consuming/command_receivers/base.rb +37 -0
  20. data/lib/emque/consuming/command_receivers/http_server.rb +103 -0
  21. data/lib/emque/consuming/command_receivers/unix_socket.rb +169 -0
  22. data/lib/emque/consuming/configuration.rb +63 -0
  23. data/lib/emque/consuming/consumer/common.rb +61 -0
  24. data/lib/emque/consuming/consumer.rb +33 -0
  25. data/lib/emque/consuming/consuming.rb +27 -0
  26. data/lib/emque/consuming/control/errors.rb +41 -0
  27. data/lib/emque/consuming/control/workers.rb +23 -0
  28. data/lib/emque/consuming/control.rb +33 -0
  29. data/lib/emque/consuming/core.rb +89 -0
  30. data/lib/emque/consuming/error_tracker.rb +39 -0
  31. data/lib/emque/consuming/generators/application.rb +95 -0
  32. data/lib/emque/consuming/helpers.rb +29 -0
  33. data/lib/emque/consuming/logging.rb +32 -0
  34. data/lib/emque/consuming/message.rb +22 -0
  35. data/lib/emque/consuming/pidfile.rb +54 -0
  36. data/lib/emque/consuming/router.rb +73 -0
  37. data/lib/emque/consuming/runner.rb +168 -0
  38. data/lib/emque/consuming/status.rb +26 -0
  39. data/lib/emque/consuming/tasks.rb +121 -0
  40. data/lib/emque/consuming/transmitter.rb +31 -0
  41. data/lib/emque/consuming/version.rb +5 -0
  42. data/lib/emque/consuming.rb +9 -0
  43. data/lib/emque-consuming.rb +3 -0
  44. data/lib/templates/.gitignore.tt +25 -0
  45. data/lib/templates/Gemfile.tt +6 -0
  46. data/lib/templates/Rakefile.tt +7 -0
  47. data/lib/templates/config/application.rb.tt +42 -0
  48. data/lib/templates/config/environments/development.rb.tt +2 -0
  49. data/lib/templates/config/environments/production.rb.tt +2 -0
  50. data/lib/templates/config/environments/staging.rb.tt +2 -0
  51. data/lib/templates/config/environments/test.rb.tt +2 -0
  52. data/lib/templates/config/routes.rb.tt +8 -0
  53. data/spec/application_spec.rb +28 -0
  54. data/spec/cli_spec.rb +136 -0
  55. data/spec/configuration_spec.rb +47 -0
  56. data/spec/consumer_spec.rb +56 -0
  57. data/spec/control/errors_spec.rb +170 -0
  58. data/spec/control_spec.rb +15 -0
  59. data/spec/core_spec.rb +121 -0
  60. data/spec/dummy/config/application.rb +38 -0
  61. data/spec/dummy/config/environments/test.rb +0 -0
  62. data/spec/dummy/config/routes.rb +0 -0
  63. data/spec/error_tracker_spec.rb +64 -0
  64. data/spec/pidfile_spec.rb +74 -0
  65. data/spec/router_spec.rb +14 -0
  66. data/spec/runner_spec.rb +138 -0
  67. data/spec/spec_helper.rb +43 -0
  68. metadata +309 -0
@@ -0,0 +1,26 @@
1
+ module Emque
2
+ module Consuming
3
+ module Adapters
4
+ module RabbitMq
5
+ def self.default_options
6
+ {
7
+ :url => "amqp://guest:guest@localhost:5672",
8
+ :prefetch => nil,
9
+ :durable => true,
10
+ :auto_delete => false
11
+ }
12
+ end
13
+
14
+ def self.load
15
+ require_relative "rabbit_mq/manager"
16
+ require_relative "rabbit_mq/worker"
17
+ require_relative "rabbit_mq/retry_worker"
18
+ end
19
+
20
+ def self.manager
21
+ Emque::Consuming::Adapters::RabbitMq::Manager
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,112 @@
1
+ require "emque/consuming/core"
2
+ require "emque/consuming/actor"
3
+ require "emque/consuming/consumer"
4
+ require "emque/consuming/error_tracker"
5
+ require "emque/consuming/message"
6
+
7
+ def emque_autoload(klass, file)
8
+ Kernel.autoload(klass, file)
9
+ end
10
+
11
+ module Emque
12
+ module Consuming
13
+ class ConfigurationError < StandardError; end
14
+
15
+ module Application
16
+ def self.included(descendant)
17
+ Emque::Consuming.application = descendant
18
+
19
+ descendant.class_eval do
20
+ extend Emque::Consuming::Core
21
+ include Emque::Consuming::Helpers
22
+
23
+ attr_reader :error_tracker, :manager
24
+
25
+ private :ensure_adapter_is_configured!, :initialize_error_tracker,
26
+ :initialize_manager, :log_prefix, :handle_shutdown
27
+ end
28
+ end
29
+
30
+ def initialize
31
+ self.class.instance = self
32
+
33
+ logger.info "#{log_prefix}: initializing"
34
+
35
+ ensure_adapter_is_configured!
36
+
37
+ initialize_manager
38
+ initialize_error_tracker
39
+ end
40
+
41
+ def notice_error(context)
42
+ error_tracker.notice_error_for(context)
43
+ verify_error_status
44
+ end
45
+
46
+ def restart
47
+ stop
48
+ initialize_manager
49
+ error_tracker.occurrences.clear
50
+ start
51
+ end
52
+
53
+ def start
54
+ logger.info "#{log_prefix}: starting"
55
+ manager.async.start
56
+ end
57
+
58
+ def stop
59
+ logger.info "#{log_prefix}: stopping"
60
+ manager.stop
61
+ end
62
+
63
+ def verify_error_status
64
+ if error_tracker.limit_reached?
65
+ handle_shutdown
66
+ runner.stop
67
+ end
68
+ end
69
+
70
+ # private
71
+
72
+ def ensure_adapter_is_configured!
73
+ if config.adapter.nil?
74
+ raise AdapterConfigurationError,
75
+ "Adapter not found! use config.set_adapter(name, options)"
76
+ end
77
+ end
78
+
79
+ def handle_shutdown
80
+ context = {
81
+ :limit => error_tracker.limit,
82
+ :expiration => error_tracker.expiration,
83
+ :occurrences => error_tracker.occurrences,
84
+ :status => runner.status.to_h,
85
+ :configuration => config.to_h
86
+ }
87
+
88
+ Emque::Consuming.logger.error("Error limit exceeded... shutting down")
89
+ Emque::Consuming.logger.error(context)
90
+
91
+ Emque::Consuming.config.shutdown_handlers.each do |handler|
92
+ handler.call(context)
93
+ end
94
+ end
95
+
96
+ def initialize_error_tracker
97
+ @error_tracker = Emque::Consuming::ErrorTracker.new(
98
+ :expiration => config.error_expiration,
99
+ :limit => config.error_limit
100
+ )
101
+ end
102
+
103
+ def initialize_manager
104
+ @manager = config.adapter.manager.new
105
+ end
106
+
107
+ def log_prefix
108
+ "#{config.app_name.capitalize} Application"
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,140 @@
1
+ require "optparse"
2
+ require "emque/consuming"
3
+ require "emque/consuming/generators/application"
4
+
5
+ module Emque
6
+ module Consuming
7
+ class Cli
8
+ APP_CONFIG_FILE = "config/application.rb"
9
+ COMMANDS = [:console, :new, :start, :stop]
10
+ IP_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/
11
+
12
+ attr_reader :options
13
+
14
+ def initialize(argv)
15
+ self.argv = argv
16
+
17
+ extract_command
18
+ intercept_help
19
+
20
+ load_app
21
+ setup_options
22
+ parse_options
23
+
24
+ execute
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :parser, :command
30
+ attr_accessor :argv, :runner
31
+
32
+ def execute
33
+ if command == :new
34
+ Emque::Consuming::Generators::Application.new(options, argv.last).generate
35
+ else
36
+ self.runner = Emque::Consuming::Runner.new(options)
37
+ runner.send(command)
38
+ end
39
+ end
40
+
41
+ def extract_command
42
+ if argv.size > 1 and argv[-2] == "new"
43
+ @command = :new
44
+ elsif argv.size > 0
45
+ @command = argv[-1].to_sym
46
+ end
47
+ end
48
+
49
+ def intercept_help
50
+ if command == :new and argv.last.to_sym == command
51
+ argv << "--help"
52
+ elsif ! COMMANDS.include?(command)
53
+ argv << "--help"
54
+ end
55
+ end
56
+
57
+ def load_app
58
+ current_dir = Dir.pwd
59
+
60
+ if File.exist?(File.join(current_dir, APP_CONFIG_FILE))
61
+ require_relative File.join(current_dir, APP_CONFIG_FILE)
62
+ end
63
+ end
64
+
65
+ def parse_options
66
+ parser.parse!(argv)
67
+ end
68
+
69
+ def setup_options
70
+ @options = {
71
+ :daemon => false
72
+ }
73
+
74
+ @parser = OptionParser.new { |o|
75
+ o.on("-P", "--pidfile PATH", "Store pid in PATH") do |arg|
76
+ options[:pidfile] = arg
77
+ end
78
+
79
+ o.on(
80
+ "-S",
81
+ "--socket PATH",
82
+ "PATH to the application's unix socket"
83
+ ) do |arg|
84
+ options[:socket_path] = arg
85
+ end
86
+
87
+ o.on(
88
+ "-b",
89
+ "--bind IP:PORT",
90
+ "IP & port for the http status application to listen on."
91
+ ) do |arg|
92
+ ip, port = arg.split(":")
93
+ port = port.to_i
94
+ options[:status_host] = ip if ip =~ IP_REGEX
95
+ options[:status_port] = port if port > 0 && port <= 65535
96
+ end
97
+
98
+ o.on("-d", "--daemon", "Daemonize the application") do
99
+ options[:daemon] = true
100
+ end
101
+
102
+ o.on(
103
+ "-e",
104
+ "--error-limit N",
105
+ "Set the max errors before application suicide"
106
+ ) do |arg|
107
+ limit = arg.to_i
108
+ options[:error_limit] = limit if limit > 0
109
+ end
110
+
111
+ o.on("-s", "--status", "Run the http status application") do
112
+ options[:status] = :on
113
+ end
114
+
115
+ o.on(
116
+ "-x",
117
+ "--error-expiration SECONDS",
118
+ "Expire errors after SECONDS"
119
+ ) do |arg|
120
+ exp = arg.to_i
121
+ options[:error_expiration] = exp if exp > 0
122
+ end
123
+
124
+ o.on("--app-name NAME", "Run the application as NAME") do |arg|
125
+ options[:app_name] = arg
126
+ end
127
+
128
+ o.on(
129
+ "--env (ex. production)",
130
+ "Set the application environment, overrides EMQUE_ENV"
131
+ ) do |arg|
132
+ options[:env] = arg
133
+ end
134
+
135
+ o.banner = "emque <options> (start|stop|new|console|help) <name (new only)>"
136
+ }
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,37 @@
1
+ module Emque
2
+ module Consuming
3
+ module CommandReceivers
4
+ NotImplemented = Class.new(StandardError)
5
+
6
+ class Base
7
+ include Emque::Consuming::Helpers
8
+
9
+ def restart
10
+ stop if running?
11
+ start
12
+ end
13
+
14
+ def start
15
+ raise NotImplemented
16
+ end
17
+
18
+ def stop
19
+ thread.exit if running?
20
+ status
21
+ end
22
+
23
+ def status
24
+ thread ? (thread.status || "stopped") : "stopped"
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :thread
30
+
31
+ def running?
32
+ thread && !thread.stop?
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,103 @@
1
+ require "puma/cli"
2
+ require "emque/consuming/command_receivers/base"
3
+
4
+ module Emque
5
+ module Consuming
6
+ module CommandReceivers
7
+ class HttpServer < Base
8
+ attr_accessor :puma
9
+
10
+ def initialize
11
+ ENV["RACK_ENV"] = Emque::Consuming.application.emque_env
12
+ initialize_puma
13
+ end
14
+
15
+ def start
16
+ puma.options[:app] = Handler.new
17
+ @thread = Thread.new { puma.run }
18
+ status
19
+ end
20
+
21
+ private
22
+
23
+ def initialize_puma
24
+ self.puma =
25
+ Puma::CLI.new(
26
+ [],
27
+ Puma::Events.new(
28
+ Logger.new(:info),
29
+ Logger.new(:error)
30
+ )
31
+ )
32
+
33
+ puma.options[:binds] = [
34
+ "tcp://#{config.status_host}"+
35
+ ":#{config.status_port}"
36
+ ]
37
+
38
+ puma.define_singleton_method :set_process_title do
39
+ # we don't want puma to take over the process name
40
+ end
41
+ end
42
+
43
+ class Handler
44
+ include Emque::Consuming::Helpers
45
+
46
+ def call(env)
47
+ req = env["REQUEST_URI"].split("/")
48
+
49
+ case req[1]
50
+ when "status"
51
+ return render_status
52
+ when "control"
53
+ case req[2]
54
+ when "errors"
55
+ if req[3..-1] && runner.control.errors(*req[3..-1]) == true
56
+ return render_status
57
+ end
58
+ else
59
+ if req[2].is_a?(String) &&
60
+ app.manager.workers.has_key?(req[2].to_sym) &&
61
+ runner.control.workers(*req[2..-1]) == true
62
+ return render_status
63
+ end
64
+ end
65
+ end
66
+
67
+ render_404
68
+ end
69
+
70
+ def render_404
71
+ [404, {}, ["Not Found"]]
72
+ end
73
+
74
+ def render_status(additional = {})
75
+ [
76
+ 200,
77
+ {},
78
+ [
79
+ Oj.dump(
80
+ runner.status.to_hsh.merge(additional),
81
+ :mode => :compat
82
+ )
83
+ ]
84
+ ]
85
+ end
86
+ end
87
+
88
+ class Logger
89
+ attr_accessor :sync, :method
90
+
91
+ def initialize(method)
92
+ self.method = method
93
+ end
94
+
95
+ def puts(str)
96
+ Emque::Consuming.logger.send(method, str)
97
+ end
98
+ alias :write :puts
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,169 @@
1
+ require "socket"
2
+ require "emque/consuming/command_receivers/base"
3
+
4
+ module Emque
5
+ module Consuming
6
+ module CommandReceivers
7
+ class UnixSocket < Base
8
+
9
+ def start
10
+ @thread = new_socket_server
11
+ status
12
+ end
13
+
14
+ private
15
+
16
+ def app_name
17
+ config.app_name.capitalize
18
+ end
19
+
20
+ def new_socket_server
21
+ Thread.new {
22
+ loop do
23
+ Socket.unix_server_loop(config.socket_path) do |sock, client_addr|
24
+ begin
25
+ receive_command(sock)
26
+ rescue
27
+ # nothing to do but restart the socket
28
+ ensure
29
+ sock.close
30
+ end
31
+ end
32
+ end
33
+ }
34
+ end
35
+
36
+ def receive_command(sock)
37
+ req = Oj.load(sock.recv(100000), :symbol_keys => true)
38
+ handler = Handler.new(req)
39
+ sock.send(handler.respond, 0)
40
+ rescue Oj::ParseError => e
41
+ sock.send(bad_request(handler), 0)
42
+ log_error(e)
43
+ rescue NoMethodError => e
44
+ sock.send(bad_request(handler), 0)
45
+ log_error(e)
46
+ rescue ArgumentError => e
47
+ sock.send(bad_request(handler), 0)
48
+ log_error(e)
49
+ rescue => e
50
+ sock.send(e.inspect, 0)
51
+ log_error(e)
52
+ end
53
+
54
+ private
55
+
56
+ def bad_request(handler)
57
+ <<-OUT
58
+ The request was not formatted properly.
59
+ We suggest using Emque::Consuming::Transmitter to send a requests.",
60
+ -------
61
+ #{handler.help rescue "Help broken"}
62
+ OUT
63
+ end
64
+
65
+ def log_error(e)
66
+ logger.error(e.inspect)
67
+ e.backtrace.each do |bt|
68
+ logger.error(bt)
69
+ end
70
+ end
71
+
72
+ class Handler
73
+ include Emque::Consuming::Helpers
74
+
75
+ COMMANDS = [:configuration, :errors, :restart, :status, :stop]
76
+
77
+ def initialize(args:, command:)
78
+ self.args = args
79
+ self.command = command.to_sym
80
+ end
81
+
82
+ def help
83
+ <<-OUT
84
+ #{app_name} Help
85
+
86
+ # Information
87
+
88
+ configuration # current configuration of the application
89
+ help # this menu
90
+ status # current status of the application
91
+
92
+ # Control
93
+
94
+ errors clear # reset the error count to 0
95
+ errors down # decrease the acceptable error threshold by 1
96
+ errors expire_after <seconds> # changes the expiration time for future errors
97
+ errors up # increase the acceptable error threshold by 1
98
+ errors retry # Reprocesses all messages in the error queue
99
+ restart # restart all workers
100
+ stop # turn the application off
101
+ -------
102
+ OUT
103
+ end
104
+
105
+ def respond
106
+ if valid_request?
107
+ method(command).call(*args)
108
+ else
109
+ help
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ attr_accessor :args, :command
116
+
117
+ def app_name
118
+ config.app_name.capitalize
119
+ end
120
+
121
+ def configuration
122
+ <<-OUT
123
+ #{app_name} Config
124
+ -------
125
+ #{config.to_hsh.map { |label, value|
126
+ "#{label}: #{value.inspect}"
127
+ }.join("\n")}
128
+ -------
129
+ OUT
130
+ end
131
+
132
+ def errors(*args)
133
+ runner.control.errors(*args) == true ? status : help
134
+ end
135
+
136
+ def restart
137
+ runner.restart_application
138
+ "The application was successfully restarted"
139
+ end
140
+
141
+ def status
142
+ data = runner.status.to_hsh
143
+ <<-OUT
144
+ #{app_name} Status
145
+ -------
146
+ errors:
147
+ #{data[:errors].map { |attr, val|
148
+ " #{attr}: #{val}"
149
+ }.join("\n")}
150
+ workers:
151
+ #{data[:workers].map { |topic, settings|
152
+ " #{topic}: #{settings[:count]}"
153
+ }.join("\n")}
154
+ -------
155
+ OUT
156
+ end
157
+
158
+ def stop
159
+ runner.stop
160
+ end
161
+
162
+ def valid_request?
163
+ COMMANDS.include?(command)
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,63 @@
1
+ require "logger"
2
+
3
+ module Emque
4
+ module Consuming
5
+ class Configuration
6
+ attr_accessor :app_name, :adapter, :error_handlers, :error_limit,
7
+ :error_expiration, :status, :status_port, :status_host, :socket_path,
8
+ :shutdown_handlers
9
+ attr_writer :env, :log_level
10
+
11
+ def initialize
12
+ @app_name = ""
13
+ @error_handlers = []
14
+ @error_limit = 5
15
+ @error_expiration = 3600 # 60 minutes
16
+ @log_level = nil
17
+ @status_port = 10000
18
+ @status_host = "0.0.0.0"
19
+ @status = :off # :on
20
+ @socket_path = "tmp/emque.sock"
21
+ @shutdown_handlers = []
22
+ end
23
+
24
+ def env
25
+ Emque::Consuming.application.emque_env
26
+ end
27
+
28
+ def env_var
29
+ @env
30
+ end
31
+
32
+ def log_level
33
+ @log_level ||= Logger::INFO
34
+ end
35
+
36
+ def set_adapter(name, options = {})
37
+ @adapter = Emque::Consuming::Adapter.new(name, options)
38
+ end
39
+
40
+ def to_hsh
41
+ {}.tap { |config|
42
+ [
43
+ :app_name,
44
+ :adapter,
45
+ :env,
46
+ :error_handlers,
47
+ :error_limit,
48
+ :error_expiration,
49
+ :log_level,
50
+ :status_port,
51
+ :status_host,
52
+ :status,
53
+ :socket_path,
54
+ :shutdown_handlers
55
+ ].each { |attr|
56
+ config[attr] = send(attr)
57
+ }
58
+ }
59
+ end
60
+ alias :to_h :to_hsh
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,61 @@
1
+ require "pipe"
2
+
3
+ module Emque
4
+ module Consuming
5
+ def self.consumer
6
+ Module.new do
7
+ define_singleton_method(:included) do |descendant|
8
+ descendant.send(:include, ::Pipe)
9
+ descendant.send(:include, ::Emque::Consuming::Consumer::Common)
10
+ end
11
+ end
12
+ end
13
+
14
+ class Consumer
15
+ module Common
16
+ def self.included(descendant)
17
+ descendant.class_eval do
18
+ attr_reader :message
19
+ private :handle_error, :pipe
20
+ end
21
+ end
22
+
23
+ def consume(handler_method, message)
24
+ send(handler_method, message)
25
+ end
26
+
27
+ def pipe_config
28
+ @pipe_config ||= Pipe::Config.new(
29
+ :error_handlers => [method(:handle_error)],
30
+ :stop_on => ->(msg, _, _) { !(msg.respond_to?(:continue?) && msg.continue?) }
31
+ )
32
+ end
33
+
34
+ def handle_error(e, method:, subject:)
35
+ context = {
36
+ :consumer => self.class.name,
37
+ :message => {
38
+ :current => subject.values,
39
+ :original => subject.original
40
+ },
41
+ :offset => subject.offset,
42
+ :partition => subject.partition,
43
+ :pipe_method => method,
44
+ :topic => subject.topic
45
+ }
46
+
47
+ # log the error by default
48
+ Emque::Consuming.logger.error("Error consuming message #{e}")
49
+ Emque::Consuming.logger.error(context)
50
+ Emque::Consuming.logger.error e.backtrace.join("\n") unless e.backtrace.nil?
51
+
52
+ Emque::Consuming.config.error_handlers.each do |handler|
53
+ handler.call(e, context)
54
+ end
55
+
56
+ Emque::Consuming.application.instance.notice_error(context)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end