queue-bus 0.7.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +21 -0
  3. data/CHANGELOG.md +30 -0
  4. data/Gemfile +4 -2
  5. data/README.mdown +16 -0
  6. data/Rakefile +2 -0
  7. data/lib/queue-bus.rb +15 -12
  8. data/lib/queue_bus/adapters/base.rb +4 -2
  9. data/lib/queue_bus/adapters/data.rb +12 -11
  10. data/lib/queue_bus/application.rb +24 -16
  11. data/lib/queue_bus/config.rb +31 -1
  12. data/lib/queue_bus/dispatch.rb +14 -12
  13. data/lib/queue_bus/dispatchers.rb +12 -5
  14. data/lib/queue_bus/driver.rb +15 -10
  15. data/lib/queue_bus/heartbeat.rb +32 -30
  16. data/lib/queue_bus/local.rb +9 -9
  17. data/lib/queue_bus/matcher.rb +36 -27
  18. data/lib/queue_bus/publisher.rb +7 -5
  19. data/lib/queue_bus/publishing.rb +31 -24
  20. data/lib/queue_bus/rider.rb +26 -22
  21. data/lib/queue_bus/subscriber.rb +20 -14
  22. data/lib/queue_bus/subscription.rb +25 -15
  23. data/lib/queue_bus/subscription_list.rb +30 -12
  24. data/lib/queue_bus/task_manager.rb +25 -16
  25. data/lib/queue_bus/tasks.rb +35 -11
  26. data/lib/queue_bus/util.rb +11 -8
  27. data/lib/queue_bus/version.rb +3 -1
  28. data/lib/queue_bus/worker.rb +3 -2
  29. data/queue-bus.gemspec +19 -18
  30. data/spec/adapter/publish_at_spec.rb +28 -25
  31. data/spec/adapter/support.rb +7 -1
  32. data/spec/adapter_spec.rb +4 -2
  33. data/spec/application_spec.rb +138 -96
  34. data/spec/config_spec.rb +35 -0
  35. data/spec/dispatch_spec.rb +48 -51
  36. data/spec/driver_spec.rb +60 -58
  37. data/spec/heartbeat_spec.rb +26 -24
  38. data/spec/integration_spec.rb +41 -40
  39. data/spec/matcher_spec.rb +104 -102
  40. data/spec/publish_spec.rb +46 -46
  41. data/spec/publisher_spec.rb +3 -1
  42. data/spec/rider_spec.rb +16 -14
  43. data/spec/spec_helper.rb +2 -2
  44. data/spec/subscriber_spec.rb +227 -227
  45. data/spec/subscription_list_spec.rb +57 -31
  46. data/spec/subscription_spec.rb +37 -36
  47. data/spec/worker_spec.rb +17 -15
  48. metadata +8 -8
@@ -1,27 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module QueueBus
2
- # queue'd in each
4
+ # The Rider is meant to execute subscriptions.
5
+ #
6
+ # When your application has subscriptions to an event we still need to enqueue some executor that
7
+ # will execute the block that was registered. The Rider will effectively ride the bus for your
8
+ # subscribed event. One Rider is launched for each subscription on an event.
3
9
  class Rider
10
+ def self.perform(attributes = {})
11
+ sub_key = attributes['bus_rider_sub_key']
12
+ app_key = attributes['bus_rider_app_key']
13
+ raise 'No application key passed' if app_key.to_s == ''
14
+ raise 'No subcription key passed' if sub_key.to_s == ''
15
+
16
+ attributes ||= {}
17
+
18
+ ::QueueBus.log_worker("Rider received: #{app_key} #{sub_key} #{attributes.inspect}")
19
+
20
+ # attributes that should be available
21
+ # attributes["bus_event_type"]
22
+ # attributes["bus_app_key"]
23
+ # attributes["bus_published_at"]
24
+ # attributes["bus_driven_at"]
4
25
 
5
- class << self
6
- def perform(attributes = {})
7
- sub_key = attributes["bus_rider_sub_key"]
8
- app_key = attributes["bus_rider_app_key"]
9
- raise "No application key passed" if app_key.to_s == ""
10
- raise "No subcription key passed" if sub_key.to_s == ""
11
-
12
- attributes ||= {}
13
-
14
- ::QueueBus.log_worker("Rider received: #{app_key} #{sub_key} #{attributes.inspect}")
15
-
16
- # attributes that should be available
17
- # attributes["bus_event_type"]
18
- # attributes["bus_app_key"]
19
- # attributes["bus_published_at"]
20
- # attributes["bus_driven_at"]
21
-
22
- # (now running with the real app that subscribed)
23
- ::QueueBus.dispatcher_execute(app_key, sub_key, attributes.merge("bus_executed_at" => Time.now.to_i))
24
- end
26
+ # (now running with the real app that subscribed)
27
+ ::QueueBus.dispatcher_execute(app_key, sub_key,
28
+ attributes.merge('bus_executed_at' => Time.now.to_i))
25
29
  end
26
30
  end
27
- end
31
+ end
@@ -1,28 +1,33 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module QueueBus
4
+ # A mixin to configure subscriptions on a particular class
2
5
  module Subscriber
3
-
4
6
  def self.included(base)
5
7
  base.extend ClassMethods
6
8
  end
7
9
 
10
+ # The class methods that will be added to the class it's included in. Use them to
11
+ # configure and subscribe.
8
12
  module ClassMethods
9
-
10
13
  def application(app_key)
11
14
  @app_key = ::QueueBus::Application.normalize(app_key)
12
15
  end
13
16
 
14
17
  def app_key
15
18
  return @app_key if @app_key
19
+
16
20
  @app_key = ::QueueBus.default_app_key
17
21
  return @app_key if @app_key
22
+
18
23
  # module or class_name
19
- val = self.name.to_s.split("::").first
24
+ val = name.to_s.split('::').first
20
25
  @app_key = ::QueueBus::Util.underscore(val)
21
26
  end
22
27
 
23
28
  def subscribe(method_name, matcher_hash = nil)
24
29
  queue_name = nil
25
- queue_name ||= self.instance_variable_get(:@queue) || (self.respond_to?(:queue) && self.queue)
30
+ queue_name ||= instance_variable_get(:@queue) || (respond_to?(:queue) && queue)
26
31
  queue_name ||= ::QueueBus.default_queue
27
32
  queue_name ||= "#{app_key}_default"
28
33
  subscribe_queue(queue_name, method_name, matcher_hash)
@@ -30,10 +35,11 @@ module QueueBus
30
35
 
31
36
  def subscribe_queue(queue_name, method_name, matcher_hash = nil)
32
37
  klass = self
33
- matcher_hash ||= {"bus_event_type" => method_name}
34
- sub_key = "#{self.name}.#{method_name}"
38
+ matcher_hash ||= { 'bus_event_type' => method_name }
39
+ sub_key = "#{name}.#{method_name}"
35
40
  dispatcher = ::QueueBus.dispatcher_by_key(app_key)
36
- dispatcher.add_subscription(queue_name, sub_key, klass.name.to_s, matcher_hash, lambda{ |att| klass.perform(att) })
41
+ dispatcher.add_subscription(queue_name, sub_key, klass.name.to_s, matcher_hash,
42
+ ->(att) { klass.perform(att) })
37
43
  end
38
44
 
39
45
  def transform(method_name)
@@ -42,8 +48,8 @@ module QueueBus
42
48
 
43
49
  def perform(attributes)
44
50
  ::QueueBus.with_global_attributes(attributes) do
45
- sub_key = attributes["bus_rider_sub_key"]
46
- meth_key = sub_key.split(".").last
51
+ sub_key = attributes['bus_rider_sub_key']
52
+ meth_key = sub_key.split('.').last
47
53
  queue_bus_execute(meth_key, attributes)
48
54
  end
49
55
  end
@@ -52,11 +58,11 @@ module QueueBus
52
58
  args = attributes
53
59
  args = send(@transform, attributes) if @transform
54
60
  args = [args] unless args.is_a?(Array)
55
- if self.respond_to?(:subscriber_with_attributes)
56
- me = self.subscriber_with_attributes(attributes)
57
- else
58
- me = self.new
59
- end
61
+ me = if respond_to?(:subscriber_with_attributes)
62
+ subscriber_with_attributes(attributes)
63
+ else
64
+ new
65
+ end
60
66
  me.send(key, *args)
61
67
  end
62
68
  end
@@ -1,29 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module QueueBus
4
+ # A Subscription is the destination of an event.
5
+ #
6
+ # The subscription can be stored in redis but should only be executed on ruby processes that
7
+ # have the application loaded. In general, this is controlled by having the background workers
8
+ # listen to specific (and discrete) queues.
2
9
  class Subscription
3
-
4
10
  class << self
5
11
  def register(queue, key, class_name, matcher, block)
6
12
  Subscription.new(queue, key, class_name, matcher, block)
7
13
  end
8
14
 
9
15
  def from_redis(hash)
10
- queue_name = hash["queue_name"].to_s
11
- key = hash["key"].to_s
12
- class_name = hash["class"].to_s
13
- matcher = hash["matcher"]
14
- return nil if key.length == 0 || queue_name.length == 0
16
+ queue_name = hash['queue_name'].to_s
17
+ key = hash['key'].to_s
18
+ class_name = hash['class'].to_s
19
+ matcher = hash['matcher']
20
+ return nil if key.empty? || queue_name.empty?
21
+
15
22
  Subscription.new(queue_name, key, class_name, matcher, nil)
16
23
  end
17
24
 
18
25
  def normalize(val)
19
- val.to_s.gsub(/\W/, "_").downcase
26
+ val.to_s.gsub(/\W/, '_').downcase
20
27
  end
21
28
  end
22
29
 
23
30
  attr_reader :matcher, :executor, :queue_name, :key, :class_name
24
- attr_accessor :app_key # dyanmically set on return from subscription_matches
31
+ attr_accessor :app_key # dyanmically set on return from subscription_matches
25
32
 
26
- def initialize(queue_name, key, class_name, filters, executor=nil)
33
+ def initialize(queue_name, key, class_name, filters, executor = nil)
27
34
  @queue_name = self.class.normalize(queue_name)
28
35
  @key = key.to_s
29
36
  @class_name = class_name.to_s
@@ -31,8 +38,12 @@ module QueueBus
31
38
  @executor = executor
32
39
  end
33
40
 
41
+ # Executes the subscription. If this is run on a server/ruby process that did not subscribe
42
+ # it will error as there will not be a proc.
34
43
  def execute!(attributes)
35
- attributes = attributes.with_indifferent_access if attributes.respond_to?(:with_indifferent_access)
44
+ if attributes.respond_to?(:with_indifferent_access)
45
+ attributes = attributes.with_indifferent_access
46
+ end
36
47
  ::QueueBus.with_global_attributes(attributes) do
37
48
  executor.call(attributes)
38
49
  end
@@ -44,12 +55,11 @@ module QueueBus
44
55
 
45
56
  def to_redis
46
57
  out = {}
47
- out["queue_name"] = queue_name
48
- out["key"] = key
49
- out["class"] = class_name
50
- out["matcher"] = matcher.to_redis
58
+ out['queue_name'] = queue_name
59
+ out['key'] = key
60
+ out['class'] = class_name
61
+ out['matcher'] = matcher.to_redis
51
62
  out
52
63
  end
53
-
54
64
  end
55
65
  end
@@ -1,11 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module QueueBus
4
+ # Manages a set of subscriptions.
5
+ #
6
+ # The subscriptions are stored in redis but not by this class. Instead this class uses two
7
+ # functions `to_redis` and `from_redis` to facilitate serialization without accessing redis
8
+ # directly.
9
+ #
10
+ # To create a new SubscriptionList, use the static function `from_redis` and pass
11
+ # it a hash that came from redis.
12
+ #
13
+ # To get a value fro redis, take your loaded SubscriptionList and call `to_redis` on it. The
14
+ # returned value can be used to store in redis.
2
15
  class SubscriptionList
3
-
4
16
  class << self
5
17
  def from_redis(redis_hash)
6
18
  out = SubscriptionList.new
7
-
8
- redis_hash.each do |key, value|
19
+
20
+ redis_hash.each do |_key, value|
9
21
  sub = Subscription.from_redis(value)
10
22
  out.add(sub) if sub
11
23
  end
@@ -21,29 +33,35 @@ module QueueBus
21
33
  end
22
34
  out
23
35
  end
24
-
36
+
25
37
  def initialize
26
38
  @subscriptions = {}
27
39
  end
28
-
40
+
29
41
  def add(sub)
30
- raise "Duplicate key: #{sub.key} already exists " \
31
- "in the #{sub.queue_name} queue!" if @subscriptions.key?(sub.key)
42
+ if @subscriptions.key?(sub.key)
43
+ raise "Duplicate key: #{sub.key} already exists " \
44
+ "in the #{sub.queue_name} queue!"
45
+ end
32
46
  @subscriptions[sub.key] = sub
33
47
  end
34
-
48
+
35
49
  def size
36
50
  @subscriptions.size
37
51
  end
38
-
52
+
53
+ def empty?
54
+ size.zero?
55
+ end
56
+
39
57
  def key(key)
40
58
  @subscriptions[key.to_s]
41
59
  end
42
-
60
+
43
61
  def all
44
62
  @subscriptions.values
45
63
  end
46
-
64
+
47
65
  def matches(attributes)
48
66
  out = []
49
67
  all.each do |sub|
@@ -52,4 +70,4 @@ module QueueBus
52
70
  out
53
71
  end
54
72
  end
55
- end
73
+ end
@@ -1,27 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module QueueBus
4
+ # A helper class for executing Rake tasks.
2
5
  class TaskManager
3
-
4
6
  attr_reader :logging
5
-
7
+
6
8
  def initialize(logging)
7
9
  @logging = logging
8
10
  end
9
-
11
+
10
12
  def subscribe!
11
13
  count = 0
12
14
  ::QueueBus.dispatchers.each do |dispatcher|
13
15
  subscriptions = dispatcher.subscriptions
14
- if subscriptions.size > 0
15
- count += subscriptions.size
16
- log "Subscribing #{dispatcher.app_key} to #{subscriptions.size} subscriptions"
17
- app = ::QueueBus::Application.new(dispatcher.app_key)
18
- app.subscribe(subscriptions, logging)
19
- log " ...done"
20
- end
16
+ next if subscriptions.empty?
17
+
18
+ count += subscriptions.size
19
+ log "Subscribing #{dispatcher.app_key} to #{subscriptions.size} subscriptions"
20
+ app = ::QueueBus::Application.new(dispatcher.app_key)
21
+ app.subscribe(subscriptions, logging)
22
+ log ' ...done'
21
23
  end
22
24
  count
23
25
  end
24
-
26
+
27
+ def unsubscribe_queue!(app_key, queue)
28
+ log "Unsubcribing #{queue} from #{app_key}"
29
+ app = ::QueueBus::Application.new(app_key)
30
+ app.unsubscribe_queue(queue)
31
+ log " ...done"
32
+ end
33
+
25
34
  def unsubscribe!
26
35
  count = 0
27
36
  ::QueueBus.dispatchers.each do |dispatcher|
@@ -29,10 +38,10 @@ module QueueBus
29
38
  app = ::QueueBus::Application.new(dispatcher.app_key)
30
39
  app.unsubscribe
31
40
  count += 1
32
- log " ...done"
41
+ log ' ...done'
33
42
  end
34
43
  end
35
-
44
+
36
45
  def queue_names
37
46
  # let's not talk to redis in here. Seems to screw things up
38
47
  queues = []
@@ -41,12 +50,12 @@ module QueueBus
41
50
  queues << sub.queue_name
42
51
  end
43
52
  end
44
-
53
+
45
54
  queues.uniq
46
55
  end
47
-
56
+
48
57
  def log(message)
49
58
  puts(message) if logging
50
59
  end
51
60
  end
52
- end
61
+ end
@@ -1,27 +1,50 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # require 'queue_bus/tasks'
2
- # will give you these tasks
4
+ # A useful set of rake tasks for managing your bus
3
5
 
6
+ # rubocop:disable Metrics/BlockLength
4
7
  namespace :queuebus do
5
-
6
- desc "Subscribes this application to QueueBus events"
7
- task :subscribe => [ :preload ] do
8
+ desc 'Subscribes this application to QueueBus events'
9
+ task subscribe: [:preload] do
8
10
  manager = ::QueueBus::TaskManager.new(true)
9
11
  count = manager.subscribe!
10
- raise "No subscriptions created" if count == 0
12
+ raise 'No subscriptions created' if count == 0
11
13
  end
12
14
 
13
15
  desc "Unsubscribes this application from QueueBus events"
14
- task :unsubscribe => [ :preload ] do
16
+ task :unsubscribe, [:app_key, :queue] => [ :preload ] do |task, args|
17
+ app_key = args[:app_key]
18
+ queue = args[:queue]
15
19
  manager = ::QueueBus::TaskManager.new(true)
16
- count = manager.unsubscribe!
17
- puts "No subscriptions unsubscribed" if count == 0
20
+
21
+ if app_key && queue
22
+ manager.unsubscribe_queue!(app_key, queue)
23
+ else
24
+ manager = ::QueueBus::TaskManager.new(true)
25
+ count = manager.unsubscribe!
26
+ puts "No subscriptions unsubscribed" if count == 0
27
+ end
18
28
  end
19
29
 
20
- desc "List QueueBus queues that need worked"
21
- task :queues => [ :preload ] do
30
+ desc 'List QueueBus queues that need worked'
31
+ task queues: [:preload] do
22
32
  manager = ::QueueBus::TaskManager.new(false)
23
33
  queues = manager.queue_names + ['bus_incoming']
24
- puts queues.join(", ")
34
+ puts queues.join(', ')
35
+ end
36
+
37
+ desc 'list time based subscriptions'
38
+ task list_scheduled: [:preload] do
39
+ scheduled_list = QueueBus::Application.all.flat_map do |app|
40
+ app.send(:subscriptions).all
41
+ .select { |s| s.matcher.filters['bus_event_type'] == 'heartbeat_minutes' }
42
+ end
43
+ scheduled_text_list = scheduled_list.collect do |e|
44
+ [e.key, e.matcher.filters['hour'] || '*', e.matcher.filters['minute'] || '*']
45
+ end
46
+ puts 'key, hour, minute'
47
+ puts scheduled_text_list.sort_by { |(_, hour, minute)| [hour.to_i, minute.to_i] }.map(&:to_csv)
25
48
  end
26
49
 
27
50
  # Preload app files if this is Rails
@@ -30,3 +53,4 @@ namespace :queuebus do
30
53
  require 'queue-bus'
31
54
  end
32
55
  end
56
+ # rubocop:enable Metrics/BlockLength
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'multi_json'
2
4
 
3
5
  module QueueBus
@@ -30,14 +32,14 @@ module QueueBus
30
32
  raise DecodeException, e.message, e.backtrace
31
33
  end
32
34
  end
33
-
35
+
34
36
  def underscore(camel_cased_word)
35
37
  word = camel_cased_word.to_s.dup
36
38
  word.gsub!('::', '/')
37
39
  # word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
38
- word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
39
- word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
40
- word.tr!("-", "_")
40
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
41
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
42
+ word.tr!('-', '_')
41
43
  word.downcase!
42
44
  word
43
45
  end
@@ -53,11 +55,11 @@ module QueueBus
53
55
  # string = string.sub(/^[a-z\d]*/) { inflections.acronyms[$&] || $&.capitalize }
54
56
  string = string.sub(/^[a-z\d]*/) { $&.capitalize }
55
57
  # string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }
56
- string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
57
- string.gsub!(/\//, '::')
58
+ string.gsub!(%r{(?:_|(/))([a-z\d]*)}i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }
59
+ string.gsub!(%r{/}, '::')
58
60
  string
59
61
  end
60
-
62
+
61
63
  def constantize(camel_cased_word)
62
64
  names = camel_cased_word.split('::')
63
65
  names.shift if names.empty? || names.first.empty?
@@ -75,6 +77,7 @@ module QueueBus
75
77
  constant = constant.ancestors.inject do |const, ancestor|
76
78
  break const if ancestor == Object
77
79
  break ancestor if ancestor.const_defined?(name, false)
80
+
78
81
  const
79
82
  end
80
83
 
@@ -84,4 +87,4 @@ module QueueBus
84
87
  end
85
88
  end
86
89
  end
87
- end
90
+ end