emque-consuming 1.0.0.beta4

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 (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