queue-bus 0.6.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +21 -0
  3. data/.rubocop.yml +35 -0
  4. data/CHANGELOG.md +38 -0
  5. data/Gemfile +4 -2
  6. data/README.mdown +16 -0
  7. data/Rakefile +2 -0
  8. data/lib/queue-bus.rb +15 -13
  9. data/lib/queue_bus/adapters/base.rb +4 -2
  10. data/lib/queue_bus/adapters/data.rb +12 -11
  11. data/lib/queue_bus/application.rb +13 -15
  12. data/lib/queue_bus/config.rb +64 -66
  13. data/lib/queue_bus/dispatch.rb +14 -12
  14. data/lib/queue_bus/dispatchers.rb +12 -5
  15. data/lib/queue_bus/driver.rb +15 -10
  16. data/lib/queue_bus/heartbeat.rb +32 -30
  17. data/lib/queue_bus/local.rb +9 -9
  18. data/lib/queue_bus/matcher.rb +36 -27
  19. data/lib/queue_bus/publisher.rb +7 -5
  20. data/lib/queue_bus/publishing.rb +31 -24
  21. data/lib/queue_bus/rider.rb +26 -22
  22. data/lib/queue_bus/subscriber.rb +20 -14
  23. data/lib/queue_bus/subscription.rb +25 -15
  24. data/lib/queue_bus/subscription_list.rb +30 -12
  25. data/lib/queue_bus/task_manager.rb +18 -16
  26. data/lib/queue_bus/tasks.rb +27 -11
  27. data/lib/queue_bus/util.rb +11 -8
  28. data/lib/queue_bus/version.rb +3 -1
  29. data/lib/queue_bus/worker.rb +3 -2
  30. data/queue-bus.gemspec +19 -17
  31. data/spec/adapter/publish_at_spec.rb +28 -25
  32. data/spec/adapter/support.rb +7 -1
  33. data/spec/adapter_spec.rb +4 -2
  34. data/spec/application_spec.rb +97 -97
  35. data/spec/config_spec.rb +116 -40
  36. data/spec/dispatch_spec.rb +48 -51
  37. data/spec/driver_spec.rb +60 -58
  38. data/spec/heartbeat_spec.rb +26 -24
  39. data/spec/integration_spec.rb +41 -40
  40. data/spec/matcher_spec.rb +104 -102
  41. data/spec/publish_spec.rb +46 -46
  42. data/spec/publisher_spec.rb +3 -1
  43. data/spec/rider_spec.rb +16 -14
  44. data/spec/spec_helper.rb +12 -7
  45. data/spec/subscriber_spec.rb +227 -227
  46. data/spec/subscription_list_spec.rb +31 -31
  47. data/spec/subscription_spec.rb +37 -36
  48. data/spec/worker_spec.rb +17 -15
  49. metadata +21 -6
@@ -1,18 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module QueueBus
4
+ # The publishing mixin provides the main interactions that users will use
5
+ # to interact with the queue bus. This module is not interacted with directly and instead
6
+ # is included inte the `QueueBus` module.
2
7
  module Publishing
3
-
4
8
  def with_global_attributes(attributes)
5
9
  original_timezone = false
6
10
  original_locale = false
7
11
 
8
- if attributes["bus_locale"] && defined?(I18n) && I18n.respond_to?(:locale=)
12
+ if attributes['bus_locale'] && defined?(I18n) && I18n.respond_to?(:locale=)
9
13
  original_locale = I18n.locale if I18n.respond_to?(:locale)
10
- I18n.locale = attributes["bus_locale"]
14
+ I18n.locale = attributes['bus_locale']
11
15
  end
12
16
 
13
- if attributes["bus_timezone"] && defined?(Time) && Time.respond_to?(:zone=)
17
+ if attributes['bus_timezone'] && defined?(Time) && Time.respond_to?(:zone=)
14
18
  original_timezone = Time.zone if Time.respond_to?(:zone)
15
- Time.zone = attributes["bus_timezone"]
19
+ Time.zone = attributes['bus_timezone']
16
20
  end
17
21
 
18
22
  yield
@@ -21,13 +25,17 @@ module QueueBus
21
25
  Time.zone = original_timezone unless original_timezone == false
22
26
  end
23
27
 
24
- def publish_metadata(event_type, attributes={})
28
+ def publish_metadata(event_type, attributes = {})
25
29
  # TODO: "bus_app_key" => application.app_key ?
26
- bus_attr = {"bus_published_at" => Time.now.to_i, "bus_event_type" => event_type}
27
- bus_attr["bus_id"] = "#{Time.now.to_i}-#{generate_uuid}"
28
- bus_attr["bus_app_hostname"] = ::QueueBus.hostname
29
- bus_attr["bus_locale"] = I18n.locale.to_s if defined?(I18n) && I18n.respond_to?(:locale) && I18n.locale
30
- bus_attr["bus_timezone"] = Time.zone.name if defined?(Time) && Time.respond_to?(:zone) && Time.zone
30
+ bus_attr = { 'bus_published_at' => Time.now.to_i, 'bus_event_type' => event_type }
31
+ bus_attr['bus_id'] = "#{Time.now.to_i}-#{generate_uuid}"
32
+ bus_attr['bus_app_hostname'] = ::QueueBus.hostname
33
+ if defined?(I18n) && I18n.respond_to?(:locale) && I18n.locale
34
+ bus_attr['bus_locale'] = I18n.locale.to_s
35
+ end
36
+ if defined?(Time) && Time.respond_to?(:zone) && Time.zone
37
+ bus_attr['bus_timezone'] = Time.zone.name
38
+ end
31
39
  out = bus_attr.merge(attributes || {})
32
40
  ::QueueBus.before_publish_callback(out)
33
41
  out
@@ -35,15 +43,14 @@ module QueueBus
35
43
 
36
44
  def generate_uuid
37
45
  require 'securerandom' unless defined?(SecureRandom)
38
- return SecureRandom.uuid
39
-
40
- rescue Exception => e
41
- # secure random not there
42
- # big random number a few times
43
- n_bytes = [42].pack('i').size
44
- n_bits = n_bytes * 8
45
- max = 2 ** (n_bits - 2) - 1
46
- return "#{rand(max)}-#{rand(max)}-#{rand(max)}"
46
+ SecureRandom.uuid
47
+ rescue Exception => e
48
+ # secure random not there
49
+ # big random number a few times
50
+ n_bytes = [42].pack('i').size
51
+ n_bits = n_bytes * 8
52
+ max = 2**(n_bits - 2) - 1
53
+ "#{rand(max)}-#{rand(max)}-#{rand(max)}"
47
54
  end
48
55
 
49
56
  def publish(event_type, attributes = {})
@@ -59,9 +66,9 @@ module QueueBus
59
66
 
60
67
  def publish_at(timestamp_or_epoch, event_type, attributes = {})
61
68
  to_publish = publish_metadata(event_type, attributes)
62
- to_publish["bus_delayed_until"] ||= timestamp_or_epoch.to_i
63
- to_publish.delete("bus_published_at") unless attributes["bus_published_at"] # will be put on when it actually does it
64
- to_publish["bus_class_proxy"] = ::QueueBus::Publisher.name.to_s
69
+ to_publish['bus_delayed_until'] ||= timestamp_or_epoch.to_i
70
+ to_publish.delete('bus_published_at') unless attributes['bus_published_at'] # will be put on when it actually does it
71
+ to_publish['bus_class_proxy'] = ::QueueBus::Publisher.name.to_s
65
72
 
66
73
  ::QueueBus.log_application("Event published:#{event_type} #{to_publish.inspect} publish_at: #{timestamp_or_epoch.to_i}")
67
74
  delayed_enqueue_to(timestamp_or_epoch.to_i, incoming_queue, ::QueueBus::Worker, to_publish)
@@ -69,7 +76,7 @@ module QueueBus
69
76
 
70
77
  def enqueue_to(queue_name, class_name, hash)
71
78
  class_name = class_name.name if class_name.is_a?(Class)
72
- hash = hash.merge("bus_class_proxy" => class_name.to_s)
79
+ hash = hash.merge('bus_class_proxy' => class_name.to_s)
73
80
  ::QueueBus.adapter.enqueue(queue_name, ::QueueBus::Worker, ::QueueBus::Util.encode(hash || {}))
74
81
  end
75
82
 
@@ -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,29 @@
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
+
25
27
  def unsubscribe!
26
28
  count = 0
27
29
  ::QueueBus.dispatchers.each do |dispatcher|
@@ -29,10 +31,10 @@ module QueueBus
29
31
  app = ::QueueBus::Application.new(dispatcher.app_key)
30
32
  app.unsubscribe
31
33
  count += 1
32
- log " ...done"
34
+ log ' ...done'
33
35
  end
34
36
  end
35
-
37
+
36
38
  def queue_names
37
39
  # let's not talk to redis in here. Seems to screw things up
38
40
  queues = []
@@ -41,12 +43,12 @@ module QueueBus
41
43
  queues << sub.queue_name
42
44
  end
43
45
  end
44
-
46
+
45
47
  queues.uniq
46
48
  end
47
-
49
+
48
50
  def log(message)
49
51
  puts(message) if logging
50
52
  end
51
53
  end
52
- end
54
+ end