queue-bus 0.5.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 (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