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