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,33 @@
1
+ require "oj"
2
+ require_relative "consumer/common"
3
+
4
+ module Emque
5
+ module Consuming
6
+ class BlockingFailure < StandardError; end
7
+
8
+ class Consumer
9
+ include Emque::Consuming.consumer
10
+
11
+ def process(message)
12
+ pipe(message, :through => [:parse, :route])
13
+ end
14
+
15
+ private
16
+
17
+ def parse(message)
18
+ message.with(
19
+ :values =>
20
+ Oj.load(message.original, :symbol_keys => true)
21
+ )
22
+ end
23
+
24
+ def route(message)
25
+ Emque::Consuming.application.router.route(
26
+ message.values.fetch(:metadata).fetch(:topic),
27
+ message.values.fetch(:metadata).fetch(:type),
28
+ message
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ require "emque/consuming/runner"
2
+ require "emque/consuming/logging"
3
+
4
+ module Emque
5
+ module Consuming
6
+ class << self
7
+ attr_accessor :application
8
+
9
+ # The Configuration instance used to configure the Emque::Consuming environment
10
+ def config
11
+ Emque::Consuming.application.config
12
+ end
13
+
14
+ def logger
15
+ Emque::Consuming::Logging.logger
16
+ end
17
+
18
+ def logger=(log)
19
+ Emque::Consuming::Logging.logger = log
20
+ end
21
+
22
+ def runner
23
+ Emque::Consuming::Runner.instance
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ module Emque
2
+ module Consuming
3
+ class Control
4
+ class Errors
5
+ include Emque::Consuming::Helpers
6
+
7
+ COMMANDS = [:clear, :down, :expire_after, :up, :retry]
8
+
9
+ def clear
10
+ app.error_tracker.occurrences.clear
11
+ end
12
+
13
+ def down
14
+ if app.error_tracker.limit > 1
15
+ config.error_limit = app.error_tracker.limit -= 1
16
+ app.verify_error_status
17
+ end
18
+ end
19
+
20
+ def expire_after(seconds)
21
+ unless seconds.is_a?(Integer)
22
+ raise ArgumentError, "first argument must be an integer"
23
+ end
24
+ config.error_expiration = app.error_tracker.expiration = seconds
25
+ end
26
+
27
+ def up
28
+ config.error_limit = app.error_tracker.limit += 1
29
+ end
30
+
31
+ def retry
32
+ app.manager.retry_errors
33
+ end
34
+
35
+ def respond_to?(method)
36
+ COMMANDS.include?(method.to_sym)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ module Emque
2
+ module Consuming
3
+ class Control
4
+ class Workers
5
+ include Emque::Consuming::Helpers
6
+
7
+ COMMANDS = [:down, :up]
8
+
9
+ def down(topic)
10
+ app.manager.worker(topic: topic.to_sym, command: :down)
11
+ end
12
+
13
+ def up(topic)
14
+ app.manager.worker(topic: topic.to_sym, command: :up)
15
+ end
16
+
17
+ def respond_to?(method)
18
+ COMMANDS.include?(method.to_sym)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ require "emque/consuming/control/errors"
2
+ require "emque/consuming/control/workers"
3
+
4
+ module Emque
5
+ module Consuming
6
+ class Control
7
+ include Emque::Consuming::Helpers
8
+
9
+ def initialize
10
+ @errors = Emque::Consuming::Control::Errors.new
11
+ @workers = Emque::Consuming::Control::Workers.new
12
+ end
13
+
14
+ def errors(*args)
15
+ if args[0] && @errors.respond_to?(args[0])
16
+ @errors.send(args.shift, *args)
17
+ true
18
+ else
19
+ @errors
20
+ end
21
+ end
22
+
23
+ def workers(topic = nil, command = nil, *args)
24
+ if command && topic && @workers.respond_to?(command)
25
+ @workers.send(command, topic, *args)
26
+ true
27
+ else
28
+ @workers
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,89 @@
1
+ require "celluloid"
2
+ require "inflecto"
3
+ require "emque/consuming/configuration"
4
+ require "emque/consuming/adapter"
5
+ require "emque/consuming/logging"
6
+ require "emque/consuming/router"
7
+ require "emque/consuming/helpers"
8
+
9
+ module Emque
10
+ module Consuming
11
+ module Core
12
+ def self.extended(descendant)
13
+ descendant.class_eval do
14
+ class << self
15
+ attr_accessor :root, :topic_mapping, :router, :instance
16
+
17
+ alias :configure :instance_exec
18
+ end
19
+ end
20
+ end
21
+
22
+ def initialize_core!
23
+ unless root
24
+ call_stack = caller_locations.map(&:path)
25
+ self.root = File.expand_path(
26
+ "../..",
27
+ call_stack.find { |call| call !~ %r{lib/emque} }
28
+ )
29
+ end
30
+
31
+ self.topic_mapping = {}
32
+
33
+ config.app_name = Inflecto.underscore(to_s).gsub("/application","")
34
+
35
+ load_app!
36
+ initialize_environment!
37
+ initialize_router!
38
+ end
39
+
40
+ def config
41
+ @config ||= Emque::Consuming::Configuration.new
42
+ end
43
+
44
+ def initialize_environment!
45
+ require_relative File.join(
46
+ root,
47
+ "config",
48
+ "environments",
49
+ "#{emque_env}.rb"
50
+ )
51
+ end
52
+
53
+ def initialize_logger
54
+ Emque::Consuming::Logging.initialize_logger(logfile)
55
+ Emque::Consuming.logger.level = config.log_level
56
+ Celluloid.logger = Emque::Consuming.logger
57
+ end
58
+
59
+ def initialize_router!
60
+ self.router ||= Emque::Consuming::Router.new
61
+ require_relative File.join(root, "config", "routes.rb")
62
+ end
63
+
64
+ def load_app!
65
+ app_files = Dir[File.join(root, "app", "**", "*.rb")]
66
+
67
+ app_files.each do |app_file|
68
+ klass = Inflecto.classify(File.basename(app_file, ".rb"))
69
+ emque_autoload(klass.to_sym, app_file)
70
+ end
71
+ end
72
+
73
+ def logfile
74
+ @logfile ||= File.join(root, "log/#{emque_env}.log").tap do |path|
75
+ directory = File.dirname(path)
76
+ Dir.mkdir(directory) unless File.exist?(directory)
77
+ end
78
+ end
79
+
80
+ def logger
81
+ Emque::Consuming.logger
82
+ end
83
+
84
+ def emque_env
85
+ config.env_var || ENV["EMQUE_ENV"] || ENV["RACK_ENV"] || "development"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,39 @@
1
+ require "digest"
2
+
3
+ module Emque
4
+ module Consuming
5
+ class ErrorTracker
6
+ attr_accessor :occurrences, :limit, :expiration
7
+
8
+ def initialize(limit: 5, expiration: 3600)
9
+ self.limit = limit
10
+ self.expiration = expiration
11
+ self.occurrences = {}
12
+ end
13
+
14
+ def notice_error_for(context)
15
+ occurrences[key_for(context)] = Time.now + expiration
16
+ end
17
+
18
+ def limit_reached?
19
+ count >= limit
20
+ end
21
+
22
+ def count
23
+ recent_errors.keys.count
24
+ end
25
+
26
+ private
27
+
28
+ def recent_errors
29
+ occurrences.delete_if do |key, expiration_time|
30
+ expiration_time < Time.now
31
+ end
32
+ end
33
+
34
+ def key_for(context)
35
+ Digest::SHA256.hexdigest(context.to_s)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,95 @@
1
+ require "erb"
2
+ require "fileutils"
3
+ require "optparse"
4
+
5
+ module Emque
6
+ module Consuming
7
+ module Generators
8
+ class Application
9
+ IGNORE = [".", ".."]
10
+
11
+ def initialize(options, name)
12
+ self.name = Inflecto.underscore(name)
13
+ self.options = options
14
+ end
15
+
16
+ def generate
17
+ context = Class.new(Object) { |obj|
18
+ def initialize(options, name)
19
+ @name = Inflecto.camelize(name)
20
+ @options = options
21
+ end
22
+
23
+ def get_binding; binding; end
24
+ }.new(options, name).get_binding
25
+
26
+ @current_dir = File.realdirpath(Dir.pwd)
27
+
28
+ recursively_copy_templates(
29
+ File.realdirpath(
30
+ File.join(
31
+ File.dirname(__FILE__),
32
+ "..",
33
+ "..",
34
+ "..",
35
+ "templates"
36
+ )
37
+ ),
38
+ [current_dir, name],
39
+ context
40
+ )
41
+ end
42
+
43
+ private
44
+
45
+ attr_accessor :name, :options
46
+ attr_reader :current_dir
47
+
48
+ def relative_path(path)
49
+ path.gsub(current_dir, ".")
50
+ end
51
+
52
+ def recursively_copy_templates(path, nesting, context)
53
+ Dir.entries(path).each do |e|
54
+ unless IGNORE.include?(e)
55
+ loc = File.join(path, e)
56
+
57
+ if Dir.exists?(loc)
58
+ new_nest = nesting + [e]
59
+ create_path = File.join(*new_nest)
60
+
61
+ unless Dir.exists?(create_path)
62
+ FileUtils.mkdir_p(create_path)
63
+ puts "created directory #{relative_path(create_path)}"
64
+ end
65
+
66
+ recursively_copy_templates(loc, new_nest, context)
67
+ elsif e =~ /\.tt$/
68
+ filename = File.join(
69
+ FileUtils.mkdir_p(File.join(*nesting)).first,
70
+ e.gsub(".tt", "")
71
+ )
72
+ display = relative_path(filename)
73
+ overwrite = "Y"
74
+
75
+ if File.exists?(filename)
76
+ print "#{display} exists, overwrite? (yN) "
77
+ overwrite = $stdin.gets
78
+ end
79
+
80
+ if overwrite.upcase.chomp == "Y"
81
+ File.open(filename, "w") do |f|
82
+ f.write(ERB.new(File.read(loc)).result(context))
83
+ end
84
+ puts "created file #{display}"
85
+ else
86
+ puts "skipping file #{display}"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,29 @@
1
+ module Emque
2
+ module Consuming
3
+ module Helpers
4
+ private
5
+
6
+ def app
7
+ @app ||=
8
+ Emque::Consuming.application.instance ||
9
+ Emque::Consuming.application.new
10
+ end
11
+
12
+ def config
13
+ Emque::Consuming.application.config
14
+ end
15
+
16
+ def logger
17
+ Emque::Consuming.application.logger
18
+ end
19
+
20
+ def router
21
+ Emque::Consuming.application.router
22
+ end
23
+
24
+ def runner
25
+ Emque::Consuming.runner
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ require "logger"
2
+
3
+ module Emque
4
+ module Consuming
5
+ module Logging
6
+ class LogFormatter < Logger::Formatter
7
+ def call(severity, time, progname, msg)
8
+ "#{time.utc} [#{severity}] #{msg}\n"
9
+ end
10
+ end
11
+
12
+ def self.initialize_logger(log_target = STDOUT)
13
+ @logger = Logger.new(log_target)
14
+ @logger.level = Logger::INFO
15
+ @logger.formatter = LogFormatter.new
16
+ @logger
17
+ end
18
+
19
+ def self.logger
20
+ defined?(@logger) ? @logger : initialize_logger
21
+ end
22
+
23
+ def self.logger=(log)
24
+ @logger = log || Logger.new("/dev/null")
25
+ end
26
+
27
+ def logger
28
+ Emque::Consuming::Logging.logger
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ require "virtus"
2
+
3
+ module Emque
4
+ module Consuming
5
+ class Message
6
+ include Virtus.value_object
7
+
8
+ values do
9
+ attribute :offset, Integer
10
+ attribute :original, Object
11
+ attribute :partition, Integer
12
+ attribute :status, Symbol, :default => :processing
13
+ attribute :topic, Symbol, :default => :unknown
14
+ attribute :values, Hash, :default => {}
15
+ end
16
+
17
+ def continue?
18
+ status == :processing
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,54 @@
1
+ require "fileutils"
2
+
3
+ module Emque
4
+ module Consuming
5
+ class Pidfile
6
+ extend Forwardable
7
+
8
+ attr_reader :path
9
+
10
+ def_delegators :pid, :to_i, :to_s
11
+
12
+ def initialize(path)
13
+ self.path = path
14
+ ensure_dir_exists
15
+ self.pid = File.read(path).to_i if File.exists?(path)
16
+ end
17
+
18
+ def running?
19
+ if pid
20
+ if pid == 0
21
+ rm_file
22
+ else
23
+ begin
24
+ Process.getpgid(pid)
25
+ return true
26
+ rescue Errno::ESRCH
27
+ rm_file
28
+ end
29
+ end
30
+ end
31
+ false
32
+ end
33
+
34
+ def write
35
+ File.open(path, "w") do |f|
36
+ f.puts Process.pid
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ attr_writer :path
43
+ attr_accessor :pid
44
+
45
+ def ensure_dir_exists
46
+ FileUtils.mkdir_p(File.dirname(path))
47
+ end
48
+
49
+ def rm_file
50
+ FileUtils.rm_f(path) if File.exists?(path)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,73 @@
1
+ module Emque
2
+ module Consuming
3
+ class Router
4
+ def initialize
5
+ self.mappings = {}
6
+ end
7
+
8
+ def map(&block)
9
+ self.instance_eval(&block)
10
+ end
11
+
12
+ def topic(mapping, &block)
13
+ mapping = Mapping.new(mapping, &block)
14
+ mappings[mapping.topic.to_sym] ||= []
15
+ mappings[mapping.topic.to_sym] << mapping
16
+ end
17
+
18
+ def route(topic, type, message)
19
+ mappings[topic.to_sym].each do |mapping|
20
+ method = mapping.route(type.to_s)
21
+
22
+ if method
23
+ consumer = mapping.consumer
24
+
25
+ consumer.new.consume(method, message)
26
+ end
27
+ end
28
+ end
29
+
30
+ def topic_mapping
31
+ mappings.inject({}) do |hash, (topic, maps)|
32
+ hash.tap do |h|
33
+ h[topic] = maps.map(&:consumer)
34
+ end
35
+ end
36
+ end
37
+
38
+ def workers(topic)
39
+ mappings[topic.to_sym].map(&:workers).max
40
+ end
41
+
42
+ private
43
+
44
+ attr_accessor :mappings
45
+
46
+ class Mapping
47
+ attr_reader :consumer, :topic, :workers
48
+
49
+ def initialize(mapping, &block)
50
+ self.topic = mapping.keys.first
51
+ self.workers = mapping.fetch(:workers, 1)
52
+ self.consumer = mapping.values.first
53
+ self.mapping = {}
54
+
55
+ self.instance_eval(&block)
56
+ end
57
+
58
+ def map(map)
59
+ mapping.merge!(map)
60
+ end
61
+
62
+ def route(type)
63
+ mapping[type]
64
+ end
65
+
66
+ private
67
+
68
+ attr_accessor :mapping
69
+ attr_writer :consumer, :topic, :workers
70
+ end
71
+ end
72
+ end
73
+ end