queue-bus 0.8.0 → 0.11.0

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +21 -0
  3. data/CHANGELOG.md +31 -0
  4. data/Gemfile +4 -2
  5. data/README.mdown +15 -3
  6. data/Rakefile +2 -0
  7. data/lib/queue-bus.rb +16 -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 +23 -2
  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 +32 -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 +36 -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 +68 -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 +12 -10
@@ -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