queue-bus 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +5 -0
  2. data/.rbenv-version +1 -0
  3. data/.rspec +1 -0
  4. data/.rvmrc +2 -0
  5. data/Gemfile +6 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.mdown +264 -0
  8. data/Rakefile +1 -0
  9. data/lib/queue-bus.rb +62 -0
  10. data/lib/queue_bus/adapters/base.rb +41 -0
  11. data/lib/queue_bus/adapters/data.rb +65 -0
  12. data/lib/queue_bus/application.rb +121 -0
  13. data/lib/queue_bus/config.rb +98 -0
  14. data/lib/queue_bus/dispatch.rb +61 -0
  15. data/lib/queue_bus/dispatchers.rb +26 -0
  16. data/lib/queue_bus/driver.rb +31 -0
  17. data/lib/queue_bus/heartbeat.rb +109 -0
  18. data/lib/queue_bus/local.rb +38 -0
  19. data/lib/queue_bus/matcher.rb +81 -0
  20. data/lib/queue_bus/publisher.rb +23 -0
  21. data/lib/queue_bus/publishing.rb +80 -0
  22. data/lib/queue_bus/rider.rb +28 -0
  23. data/lib/queue_bus/subscriber.rb +65 -0
  24. data/lib/queue_bus/subscription.rb +55 -0
  25. data/lib/queue_bus/subscription_list.rb +53 -0
  26. data/lib/queue_bus/task_manager.rb +52 -0
  27. data/lib/queue_bus/util.rb +87 -0
  28. data/lib/queue_bus/version.rb +3 -0
  29. data/lib/queue_bus/worker.rb +14 -0
  30. data/lib/tasks/resquebus.rake +2 -0
  31. data/queue-bus.gemspec +32 -0
  32. data/spec/adapter/publish_at_spec.rb +48 -0
  33. data/spec/adapter/support.rb +15 -0
  34. data/spec/adapter_spec.rb +14 -0
  35. data/spec/application_spec.rb +152 -0
  36. data/spec/config_spec.rb +83 -0
  37. data/spec/dispatch_spec.rb +76 -0
  38. data/spec/driver_spec.rb +100 -0
  39. data/spec/heartbeat_spec.rb +44 -0
  40. data/spec/integration_spec.rb +53 -0
  41. data/spec/matcher_spec.rb +143 -0
  42. data/spec/publish_spec.rb +95 -0
  43. data/spec/publisher_spec.rb +7 -0
  44. data/spec/rider_spec.rb +39 -0
  45. data/spec/spec_helper.rb +69 -0
  46. data/spec/subscriber_spec.rb +268 -0
  47. data/spec/subscription_list_spec.rb +43 -0
  48. data/spec/subscription_spec.rb +53 -0
  49. metadata +192 -0
@@ -0,0 +1,61 @@
1
+ # Creates a DSL for apps to define their blocks to run for event_types
2
+
3
+ module QueueBus
4
+ class Dispatch
5
+
6
+ attr_reader :app_key, :subscriptions
7
+
8
+ def initialize(app_key)
9
+ @app_key = Application.normalize(app_key)
10
+ @subscriptions = SubscriptionList.new
11
+ end
12
+
13
+ def size
14
+ @subscriptions.size
15
+ end
16
+
17
+ def subscribe(key, matcher_hash = nil, &block)
18
+ dispatch_event("default", key, matcher_hash, block)
19
+ end
20
+
21
+ # allows definitions of other queues
22
+ def method_missing(method_name, *args, &block)
23
+ if args.size == 1 && block
24
+ dispatch_event(method_name, args[0], nil, block)
25
+ elsif args.size == 2 && block
26
+ dispatch_event(method_name, args[0], args[1], block)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def execute(key, attributes)
33
+ sub = subscriptions.key(key)
34
+ if sub
35
+ sub.execute!(attributes)
36
+ else
37
+ # TODO: log that it's not there
38
+ end
39
+ end
40
+
41
+ def subscription_matches(attributes)
42
+ out = subscriptions.matches(attributes)
43
+ out.each do |sub|
44
+ sub.app_key = self.app_key
45
+ end
46
+ out
47
+ end
48
+
49
+ def dispatch_event(queue, key, matcher_hash, block)
50
+ # if not matcher_hash, assume key is a event_type regex
51
+ matcher_hash ||= { "bus_event_type" => key }
52
+ add_subscription("#{app_key}_#{queue}", key, "::QueueBus::Rider", matcher_hash, block)
53
+ end
54
+
55
+ def add_subscription(queue_name, key, class_name, matcher_hash = nil, block)
56
+ sub = Subscription.register(queue_name, key, class_name, matcher_hash, block)
57
+ subscriptions.add(sub)
58
+ sub
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,26 @@
1
+ module QueueBus
2
+ class Dispatchers
3
+ def dispatch(app_key=nil, &block)
4
+ dispatcher = dispatcher_by_key(app_key)
5
+ dispatcher.instance_eval(&block)
6
+ dispatcher
7
+ end
8
+
9
+ def dispatchers
10
+ @dispatchers ||= {}
11
+ @dispatchers.values
12
+ end
13
+
14
+ def dispatcher_by_key(app_key)
15
+ app_key = Application.normalize(app_key || ::QueueBus.default_app_key)
16
+ @dispatchers ||= {}
17
+ @dispatchers[app_key] ||= Dispatch.new(app_key)
18
+ end
19
+
20
+ def dispatcher_execute(app_key, key, attributes)
21
+ @dispatchers ||= {}
22
+ dispatcher = @dispatchers[app_key]
23
+ dispatcher.execute(key, attributes) if dispatcher
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ module QueueBus
2
+ # fans out an event to multiple queues
3
+ class Driver
4
+ include ::QueueBus::Worker
5
+
6
+ class << self
7
+ def subscription_matches(attributes)
8
+ out = []
9
+ Application.all.each do |app|
10
+ subs = app.subscription_matches(attributes)
11
+ out.concat(subs)
12
+ end
13
+ out
14
+ end
15
+
16
+ def perform(attributes={})
17
+ raise "No attributes passed" if attributes.empty?
18
+
19
+ ::QueueBus.log_worker("Driver running: #{attributes.inspect}")
20
+
21
+ subscription_matches(attributes).each do |sub|
22
+ ::QueueBus.log_worker(" ...sending to #{sub.queue_name} queue with class #{sub.class_name} for app #{sub.app_key} because of subscription: #{sub.key}")
23
+
24
+ bus_attr = {"bus_driven_at" => Time.now.to_i, "bus_rider_queue" => sub.queue_name, "bus_rider_app_key" => sub.app_key, "bus_rider_sub_key" => sub.key, "bus_rider_class_name" => sub.class_name}
25
+ ::QueueBus.enqueue_to(sub.queue_name, sub.class_name, bus_attr.merge(attributes || {}))
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,109 @@
1
+ module QueueBus
2
+ # publishes event about the current time
3
+ class Heartbeat
4
+ include ::QueueBus::Worker
5
+
6
+ class << self
7
+
8
+ def lock_key
9
+ "bus:heartbeat:lock"
10
+ end
11
+
12
+ def lock_seconds
13
+ 60
14
+ end
15
+
16
+ def lock!
17
+ now = Time.now.to_i
18
+ timeout = now + lock_seconds + 2
19
+
20
+ ::QueueBus.redis do |redis|
21
+ # return true if we successfully acquired the lock
22
+ return timeout if redis.setnx(lock_key, timeout)
23
+
24
+ # see if the existing timeout is still valid and return false if it is
25
+ # (we cannot acquire the lock during the timeout period)
26
+ return 0 if now <= redis.get(lock_key).to_i
27
+
28
+ # otherwise set the timeout and ensure that no other worker has
29
+ # acquired the lock
30
+ if now > redis.getset(lock_key, timeout).to_i
31
+ return timeout
32
+ else
33
+ return 0
34
+ end
35
+ end
36
+ end
37
+
38
+ def unlock!
39
+ ::QueueBus.redis { |redis| redis.del(lock_key) }
40
+ end
41
+
42
+
43
+ def redis_key
44
+ "bus:heartbeat:timestamp"
45
+ end
46
+
47
+ def environment_name
48
+ ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["BUS_ENV"]
49
+ end
50
+
51
+ def get_saved_minute!
52
+ key = ::QueueBus.redis { |redis| redis.get(redis_key) }
53
+ return nil if key.nil?
54
+ case environment_name
55
+ when 'development', 'test'
56
+ # only 3 minutes in development; otherwise, TONS of events if not run in a while
57
+ three_ago = Time.now.to_i - 3*60*60
58
+ key = three_ago if key.to_i < three_ago
59
+ end
60
+ return key.to_i
61
+ end
62
+
63
+ def set_saved_minute!(epoch_minute)
64
+ ::QueueBus.redis { |redis| redis.set(redis_key, epoch_minute) }
65
+ end
66
+
67
+ def perform
68
+ real_now = Time.now.to_i
69
+ run_until = lock! - 2
70
+ return if run_until < real_now
71
+
72
+ while((real_now = Time.now.to_i) < run_until)
73
+ minutes = real_now.to_i / 60
74
+ last = get_saved_minute!
75
+ if last
76
+ break if minutes <= last
77
+ minutes = last + 1
78
+ end
79
+
80
+ seconds = minutes * (60)
81
+ hours = minutes / (60)
82
+ days = minutes / (60*24)
83
+
84
+ now = Time.at(seconds)
85
+
86
+ attributes = {}
87
+ attributes["epoch_seconds"] = seconds
88
+ attributes["epoch_minutes"] = minutes
89
+ attributes["epoch_hours"] = hours
90
+ attributes["epoch_days"] = days
91
+
92
+ attributes["minute"] = now.min
93
+ attributes["hour"] = now.hour
94
+ attributes["day"] = now.day
95
+ attributes["month"] = now.month
96
+ attributes["year"] = now.year
97
+ attributes["yday"] = now.yday
98
+ attributes["wday"] = now.wday
99
+
100
+ ::QueueBus.publish("heartbeat_minutes", attributes)
101
+ set_saved_minute!(minutes)
102
+ end
103
+
104
+ unlock!
105
+ end
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,38 @@
1
+ module QueueBus
2
+ # only process local queues
3
+ class Local
4
+
5
+ class << self
6
+ def perform(attributes = {})
7
+ if ::QueueBus.local_mode == :suppress
8
+ ::QueueBus.log_worker("Suppressed: #{attributes.inspect}")
9
+ return # not doing anything
10
+ end
11
+
12
+ ::QueueBus.log_worker("Local running: #{attributes.inspect}")
13
+
14
+ # looking for subscriptions, not queues
15
+ subscription_matches(attributes).each do |sub|
16
+ bus_attr = {"bus_driven_at" => Time.now.to_i, "bus_rider_queue" => sub.queue_name, "bus_rider_app_key" => sub.app_key, "bus_rider_sub_key" => sub.key, "bus_rider_class_name" => sub.class_name}
17
+ to_publish = bus_attr.merge(attributes || {})
18
+ if ::QueueBus.local_mode == :standalone
19
+ ::QueueBus.enqueue_to(sub.queue_name, sub.class_name, bus_attr.merge(attributes || {}))
20
+ else # defaults to inline mode
21
+ sub.execute!(to_publish)
22
+ end
23
+ end
24
+ end
25
+
26
+ # looking directly at subscriptions loaded into dispatcher
27
+ # so we don't need redis server up
28
+ def subscription_matches(attributes)
29
+ out = []
30
+ ::QueueBus.dispatchers.each do |dispatcher|
31
+ out.concat(dispatcher.subscription_matches(attributes))
32
+ end
33
+ out
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,81 @@
1
+ module QueueBus
2
+ class Matcher
3
+ SPECIAL_PREPEND = "bus_special_value_"
4
+ attr_reader :filters
5
+ def initialize(hash)
6
+ @filters = encode(hash)
7
+ end
8
+
9
+ def to_redis
10
+ @filters
11
+ end
12
+
13
+ def match?(attribute_name, attributes)
14
+ mine = filters[attribute_name].to_s
15
+ return false if mine.size == 0
16
+
17
+ given = attributes[attribute_name]
18
+ case mine
19
+ when "#{SPECIAL_PREPEND}key"
20
+ return true if attributes.has_key?(attribute_name)
21
+ return false
22
+ when "#{SPECIAL_PREPEND}blank"
23
+ return true if given.to_s.strip.size == 0
24
+ return false
25
+ when "#{SPECIAL_PREPEND}empty"
26
+ return false if given == nil
27
+ return true if given.to_s.size == 0
28
+ return false
29
+ when "#{SPECIAL_PREPEND}nil"
30
+ return true if given == nil
31
+ return false
32
+ when "#{SPECIAL_PREPEND}value"
33
+ return false if given == nil
34
+ return true
35
+ when "#{SPECIAL_PREPEND}present"
36
+ return true if given.to_s.strip.size > 0
37
+ return false
38
+ end
39
+
40
+ given = given.to_s
41
+
42
+ return true if mine == given
43
+ begin
44
+ # if it's already a regex, don't mess with it
45
+ # otherwise, it should have start and end line situation
46
+ if mine[0..6] == "(?-mix:"
47
+ regex = Regexp.new(mine)
48
+ else
49
+ regex = Regexp.new("^#{mine}$")
50
+ end
51
+ return !!regex.match(given)
52
+ rescue
53
+ return false
54
+ end
55
+ end
56
+
57
+ def matches?(attributes)
58
+ return false if filters.empty?
59
+ return false if attributes == nil
60
+
61
+ filters.keys.each do |key|
62
+ return false unless match?(key, attributes)
63
+ end
64
+
65
+ true
66
+ end
67
+
68
+ def encode(hash)
69
+ out = {}
70
+ hash.each do |key, value|
71
+ case value
72
+ when :key, :blank, :nil, :present, :empty, :value
73
+ value = "#{SPECIAL_PREPEND}#{value}"
74
+ end
75
+ out[key.to_s] = value.to_s
76
+ end
77
+ out
78
+ end
79
+ end
80
+ end
81
+
@@ -0,0 +1,23 @@
1
+ module QueueBus
2
+ # publishes on a delay
3
+ class Publisher
4
+ include ::QueueBus::Worker
5
+
6
+ class << self
7
+ def perform(*args)
8
+ # TODO: move this to just resquebus for fallback
9
+ if args.size > 1
10
+ # handles older arguments
11
+ event_type = args.first
12
+ attributes = args.last
13
+ else
14
+ attributes = args.first
15
+ event_type = attributes["bus_event_type"]
16
+ end
17
+ ::QueueBus.log_worker("Publisher running: #{event_type} - #{attributes.inspect}")
18
+ ::QueueBus.publish(event_type, attributes)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,80 @@
1
+ module QueueBus
2
+ module Publishing
3
+
4
+ def with_global_attributes(attributes)
5
+ original_timezone = false
6
+ original_locale = false
7
+
8
+ if attributes["bus_locale"] && defined?(I18n) && I18n.respond_to?(:locale=)
9
+ original_locale = I18n.locale if I18n.respond_to?(:locale)
10
+ I18n.locale = attributes["bus_locale"]
11
+ end
12
+
13
+ if attributes["bus_timezone"] && defined?(Time) && Time.respond_to?(:zone=)
14
+ original_timezone = Time.zone if Time.respond_to?(:zone)
15
+ Time.zone = attributes["bus_timezone"]
16
+ end
17
+
18
+ yield
19
+ ensure
20
+ I18n.locale = original_locale unless original_locale == false
21
+ Time.zone = original_timezone unless original_timezone == false
22
+ end
23
+
24
+ def publish_metadata(event_type, attributes={})
25
+ # 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
31
+ out = bus_attr.merge(attributes || {})
32
+ ::QueueBus.before_publish_callback(out)
33
+ out
34
+ end
35
+
36
+ def generate_uuid
37
+ 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)}"
47
+ end
48
+
49
+ def publish(event_type, attributes = {})
50
+ to_publish = publish_metadata(event_type, attributes)
51
+ ::QueueBus.log_application("Event published: #{event_type} #{to_publish.inspect}")
52
+ if local_mode
53
+ ::QueueBus::Local.perform(to_publish)
54
+ else
55
+ enqueue_to(::QueueBus.incoming_queue, Driver, to_publish)
56
+ end
57
+ end
58
+
59
+ def publish_at(timestamp_or_epoch, event_type, attributes = {})
60
+ to_publish = publish_metadata(event_type, attributes)
61
+ to_publish["bus_delayed_until"] ||= timestamp_or_epoch.to_i
62
+ to_publish.delete("bus_published_at") unless attributes["bus_published_at"] # will be put on when it actually does it
63
+
64
+ ::QueueBus.log_application("Event published:#{event_type} #{to_publish.inspect} publish_at: #{timestamp_or_epoch.to_i}")
65
+ delayed_enqueue_to(timestamp_or_epoch.to_i, incoming_queue, Publisher, to_publish)
66
+ end
67
+
68
+ def enqueue_to(queue_name, klass, hash)
69
+ ::QueueBus.adapter.enqueue(queue_name, klass, hash)
70
+ end
71
+
72
+ def delayed_enqueue_to(epoch_seconds, queue_name, klass, hash)
73
+ ::QueueBus.adapter.enqueue_at(epoch_seconds, queue_name, klass, hash)
74
+ end
75
+
76
+ def heartbeat!
77
+ ::QueueBus.adapter.setup_heartbeat!(incoming_queue)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,28 @@
1
+ module QueueBus
2
+ # queue'd in each
3
+ class Rider
4
+ include ::QueueBus::Worker
5
+
6
+ class << self
7
+ def perform(attributes = {})
8
+ sub_key = attributes["bus_rider_sub_key"]
9
+ app_key = attributes["bus_rider_app_key"]
10
+ raise "No application key passed" if app_key.to_s == ""
11
+ raise "No subcription key passed" if sub_key.to_s == ""
12
+
13
+ attributes ||= {}
14
+
15
+ ::QueueBus.log_worker("Rider received: #{app_key} #{sub_key} #{attributes.inspect}")
16
+
17
+ # attributes that should be available
18
+ # attributes["bus_event_type"]
19
+ # attributes["bus_app_key"]
20
+ # attributes["bus_published_at"]
21
+ # attributes["bus_driven_at"]
22
+
23
+ # (now running with the real app that subscribed)
24
+ ::QueueBus.dispatcher_execute(app_key, sub_key, attributes.merge("bus_executed_at" => Time.now.to_i))
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,65 @@
1
+ module QueueBus
2
+ module Subscriber
3
+
4
+ def self.included(base)
5
+ base.send(:include, ::QueueBus::Worker)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ def application(app_key)
12
+ @app_key = ::QueueBus::Application.normalize(app_key)
13
+ end
14
+
15
+ def app_key
16
+ return @app_key if @app_key
17
+ @app_key = ::QueueBus.default_app_key
18
+ return @app_key if @app_key
19
+ # module or class_name
20
+ val = self.name.to_s.split("::").first
21
+ @app_key = ::QueueBus::Util.underscore(val)
22
+ end
23
+
24
+ def subscribe(method_name, matcher_hash = nil)
25
+ queue_name = nil
26
+ queue_name ||= self.instance_variable_get(:@queue) || (self.respond_to?(:queue) && self.queue)
27
+ queue_name ||= ::QueueBus.default_queue
28
+ queue_name ||= "#{app_key}_default"
29
+ subscribe_queue(queue_name, method_name, matcher_hash)
30
+ end
31
+
32
+ def subscribe_queue(queue_name, method_name, matcher_hash = nil)
33
+ klass = self
34
+ matcher_hash ||= {"bus_event_type" => method_name}
35
+ sub_key = "#{self.name}.#{method_name}"
36
+ dispatcher = ::QueueBus.dispatcher_by_key(app_key)
37
+ dispatcher.add_subscription(queue_name, sub_key, klass.name.to_s, matcher_hash, lambda{ |att| klass.perform(att) })
38
+ end
39
+
40
+ def transform(method_name)
41
+ @transform = method_name
42
+ end
43
+
44
+ def perform(attributes)
45
+ ::QueueBus.with_global_attributes(attributes) do
46
+ sub_key = attributes["bus_rider_sub_key"]
47
+ meth_key = sub_key.split(".").last
48
+ queue_bus_execute(meth_key, attributes)
49
+ end
50
+ end
51
+
52
+ def queue_bus_execute(key, attributes)
53
+ args = attributes
54
+ args = send(@transform, attributes) if @transform
55
+ args = [args] unless args.is_a?(Array)
56
+ if self.respond_to?(:subscriber_with_attributes)
57
+ me = self.subscriber_with_attributes(attributes)
58
+ else
59
+ me = self.new
60
+ end
61
+ me.send(key, *args)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,55 @@
1
+ module QueueBus
2
+ class Subscription
3
+
4
+ class << self
5
+ def register(queue, key, class_name, matcher, block)
6
+ Subscription.new(queue, key, class_name, matcher, block)
7
+ end
8
+
9
+ 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
15
+ Subscription.new(queue_name, key, class_name, matcher, nil)
16
+ end
17
+
18
+ def normalize(val)
19
+ val.to_s.gsub(/\W/, "_").downcase
20
+ end
21
+ end
22
+
23
+ attr_reader :matcher, :executor, :queue_name, :key, :class_name
24
+ attr_accessor :app_key # dyanmically set on return from subscription_matches
25
+
26
+ def initialize(queue_name, key, class_name, filters, executor=nil)
27
+ @queue_name = self.class.normalize(queue_name)
28
+ @key = key.to_s
29
+ @class_name = class_name.to_s
30
+ @matcher = Matcher.new(filters)
31
+ @executor = executor
32
+ end
33
+
34
+ def execute!(attributes)
35
+ attributes = attributes.with_indifferent_access if attributes.respond_to?(:with_indifferent_access)
36
+ ::QueueBus.with_global_attributes(attributes) do
37
+ executor.call(attributes)
38
+ end
39
+ end
40
+
41
+ def matches?(attributes)
42
+ @matcher.matches?(attributes)
43
+ end
44
+
45
+ def to_redis
46
+ out = {}
47
+ out["queue_name"] = queue_name
48
+ out["key"] = key
49
+ out["class"] = class_name
50
+ out["matcher"] = matcher.to_redis
51
+ out
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,53 @@
1
+ module QueueBus
2
+ class SubscriptionList
3
+
4
+ class << self
5
+ def from_redis(redis_hash)
6
+ out = SubscriptionList.new
7
+
8
+ redis_hash.each do |key, value|
9
+ sub = Subscription.from_redis(value)
10
+ out.add(sub) if sub
11
+ end
12
+
13
+ out
14
+ end
15
+ end
16
+
17
+ def to_redis
18
+ out = {}
19
+ @subscriptions.values.each do |sub|
20
+ out[sub.key] = sub.to_redis
21
+ end
22
+ out
23
+ end
24
+
25
+ def initialize
26
+ @subscriptions = {}
27
+ end
28
+
29
+ def add(sub)
30
+ @subscriptions[sub.key] = sub
31
+ end
32
+
33
+ def size
34
+ @subscriptions.size
35
+ end
36
+
37
+ def key(key)
38
+ @subscriptions[key.to_s]
39
+ end
40
+
41
+ def all
42
+ @subscriptions.values
43
+ end
44
+
45
+ def matches(attributes)
46
+ out = []
47
+ all.each do |sub|
48
+ out << sub if sub.matches?(attributes)
49
+ end
50
+ out
51
+ end
52
+ end
53
+ end