ribbon-event_bus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c83efffe844e2c1892b1e481097a5831aedf70ee
4
+ data.tar.gz: 186b25081e035b0b0ce0a2eb471fea5aee74b260
5
+ SHA512:
6
+ metadata.gz: ffd5b13af305094619bfc9b9fd51fa3df095b2fee5b1a037b4f79eebd09ad0848661d78e96d27f44aadf3af882fa709cdd71895f9e253ee5ca94551220ebf54d
7
+ data.tar.gz: 6c9ef7a77f6b8934edb74e206d7ae785ddf9738fe4f5d72c89602f9d8d668653a1b1030b3d60fb3dc37a71f0f05fbdce6190d138e5cb2cbc736ae9caba5c34d9
@@ -0,0 +1,12 @@
1
+ subscriptions:
2
+ default_priority: medium
3
+
4
+ publishers:
5
+ resque:
6
+ publisher_queue: publisher
7
+ subscription_queue_format: 'subscriptions_p%{priority}'
8
+
9
+ remote_resque:
10
+ queue: publisher
11
+ redis_namespace: resque
12
+ subscription_queue_format: 'subscriptions_p%{priority}'
@@ -0,0 +1,134 @@
1
+ require 'yaml'
2
+
3
+ module Ribbon::EventBus
4
+ class Config
5
+ # Putting this here instead of in Errors because I may move this into a
6
+ # separate gem.
7
+ class ConfigError < StandardError; end
8
+ class NotAddableError < ConfigError; end
9
+
10
+ class ProcArray < Array
11
+ def push(*procs)
12
+ raise ConfigError, "May only add blocks" unless procs.all? { |p| p.is_a?(Proc) }
13
+ super
14
+ end
15
+
16
+ def call(*args, &block)
17
+ map { |x| x.call(*args, &block) }
18
+ end
19
+ end
20
+
21
+ attr_reader :name
22
+
23
+ def initialize(name=nil, &block)
24
+ @name = name
25
+ define(&block) if block_given?
26
+ end
27
+
28
+ def define(&block)
29
+ case block.arity
30
+ when 0 then instance_eval(&block)
31
+ when 1 then block.call(self)
32
+ else raise ConfigError, 'invalid config block arity'
33
+ end
34
+ end
35
+
36
+ # Deep dup all the values.
37
+ def dup
38
+ super.tap do |new_config|
39
+ new_config.instance_variable_set(
40
+ :@_nested,
41
+ Hash[
42
+ _nested.map { |key, object|
43
+ [
44
+ key,
45
+ object.is_a?(Config) ? object.dup : object
46
+ ]
47
+ }
48
+ ]
49
+ )
50
+ end
51
+ end
52
+
53
+ def method_missing(meth, *args, &block)
54
+ meth_str = meth.to_s
55
+
56
+ if /^(\w+)\=$/.match(meth_str)
57
+ _set($1, *args, &block)
58
+ elsif args.length > 0 || block_given?
59
+ _add(meth, *args, &block)
60
+ elsif /^(\w+)\?$/.match(meth_str)
61
+ !!_get($1)
62
+ else
63
+ object = _get(meth)
64
+ object = _set(meth, Config.new((name ? "#{name}." : '') + meth_str)) unless object
65
+ object
66
+ end
67
+ end
68
+
69
+ def nested(name)
70
+ _nested[name.to_sym]
71
+ end
72
+
73
+ def nest_value(name, value)
74
+ nested()
75
+ end
76
+
77
+ def nested_values(key)
78
+ _nested_values[key.to_sym]
79
+ end
80
+
81
+ def nested_configs(namespace)
82
+ _nested_configs[namespace.to_sym]
83
+ end
84
+
85
+ def merge_hash!(hash)
86
+ hash.each { |k, v|
87
+ if v.is_a?(Hash)
88
+ send(k).merge_hash!(v)
89
+ else
90
+ send("#{k}=", v)
91
+ end
92
+ }
93
+
94
+ self
95
+ end
96
+
97
+ def merge_config_file!(file)
98
+ merge_hash!(YAML.load_file(file))
99
+ end
100
+
101
+ private
102
+ def _get(key)
103
+ _nested[key.to_sym]
104
+ end
105
+
106
+ def _set(key, *args, &block)
107
+ object = _args_to_object(*args, &block)
108
+ _nested[key.to_sym] = object
109
+ end
110
+
111
+ def _add(key, *args, &block)
112
+ raise NotAddableError, self.inspect if @_value && !@_value.is_a?(Array)
113
+ object = _args_to_object(*args, &block)
114
+ _set(key, object.is_a?(Proc) ? ProcArray.new : []) unless _get(key)
115
+ _get(key).push(object)
116
+ end
117
+
118
+ def _args_to_object(*args, &block)
119
+ if args.length == 1
120
+ args.first
121
+ elsif args.length > 1
122
+ args
123
+ elsif block_given?
124
+ block
125
+ else
126
+ raise ConfigError, 'must pass value or block'
127
+ end
128
+ end
129
+
130
+ def _nested
131
+ @_nested ||= {}
132
+ end
133
+ end # Config
134
+ end # Ribbon::EventBus
@@ -0,0 +1,49 @@
1
+ module Ribbon::EventBus
2
+ module Errors
3
+ class Error < StandardError; end
4
+ class DuplicateInstanceNameError < Error; end
5
+ class NoPublishersDefinedError < Error; end
6
+
7
+ ###
8
+ # Instance Errors
9
+ ###
10
+ class InstanceError < Error; end
11
+
12
+ ###
13
+ # Event Errors
14
+ ###
15
+ class EventError < Error; end
16
+ class UnsafeValueError < EventError
17
+ def initialize(key, value)
18
+ super("#{key.inspect} => #{value.inspect}")
19
+ end
20
+ end # UnsafeValueError
21
+
22
+ ###
23
+ # Subscription Errors
24
+ ###
25
+ class SubscriptionError < Error; end
26
+ class InvalidPriorityError < SubscriptionError; end
27
+ class UnexpectedEventError < SubscriptionError; end
28
+
29
+ ###
30
+ # Publisher Errors
31
+ ###
32
+ class PublisherError < Error; end
33
+ class InvalidPublisherError < PublisherError; end
34
+ class InvalidPublisherNameError < PublisherError; end
35
+
36
+ # RemoteResquePublisher Errors
37
+ class RemoteResquePublisherError < PublisherError; end
38
+
39
+ # ProcPublisherErrors
40
+ class ProcPublisherError < PublisherError; end
41
+ class MissingProcError < ProcPublisherError; end
42
+ class InvalidArityError < ProcPublisherError; end
43
+
44
+ ###
45
+ # Serializable Errors
46
+ ###
47
+ class SerializableError < Error; end
48
+ end
49
+ end
@@ -0,0 +1,85 @@
1
+ module Ribbon::EventBus
2
+ class Event
3
+ include Mixins::HasInstance
4
+ include Mixins::HasConfig
5
+ include Mixins::Serializable
6
+
7
+ config_key :events
8
+ serialize_with :name, :instance, :params
9
+
10
+ attr_reader :name
11
+ attr_reader :params
12
+
13
+ def initialize(name, params={})
14
+ raise ArgumentError, 'missing event name' unless name.respond_to?(:to_sym)
15
+ @name = name.to_sym
16
+ _evaluate_params(params)
17
+ end
18
+
19
+ def self.load_from_serialized(name, instance, params)
20
+ new(name, params.merge(instance: instance))
21
+ end
22
+
23
+ def [](key)
24
+ params[key]
25
+ end
26
+
27
+ def publish
28
+ instance.publishers.each { |p| p.publish(self) }
29
+ end
30
+
31
+ def subscriptions
32
+ instance.subscriptions_to(self)
33
+ end
34
+
35
+ private
36
+
37
+ ############################################################################
38
+ # Parameter Evaluation Logic
39
+ #
40
+ # This evaluates the parameters passed to the initializer.
41
+ ############################################################################
42
+
43
+ ###
44
+ # Root evaluation method.
45
+ ###
46
+ def _evaluate_params(params)
47
+ unless params.is_a?(Hash)
48
+ raise ArgumentError, 'event parameters must be a hash'
49
+ end
50
+
51
+ params = params.dup
52
+ @instance = params.delete(:instance)
53
+ @params = _sanitize_params(params)
54
+ end
55
+
56
+ ###
57
+ # Sanitize the event params.
58
+ # Prevents passing values that could cause errors later in the EventBus.
59
+ ###
60
+ def _sanitize_params(params)
61
+ Hash[params.map { |key, value| [key.to_sym, _sanitize_value(key, value)] }].freeze
62
+ end
63
+
64
+ # Sanitize an array.
65
+ def _sanitize_array(key, array)
66
+ array.map { |value| _sanitize_value(key, value) }.freeze
67
+ end
68
+
69
+ # Sanitize an individual value.
70
+ def _sanitize_value(key, value)
71
+ case value
72
+ when String
73
+ value.dup.freeze
74
+ when Symbol, Integer, Float, NilClass, TrueClass, FalseClass
75
+ value
76
+ when Array
77
+ _sanitize_array(key, value)
78
+ when Hash
79
+ _sanitize_params(value)
80
+ else
81
+ raise Errors::UnsafeValueError.new(key, value)
82
+ end
83
+ end
84
+ end # Event
85
+ end # Ribbon::EventBus
@@ -0,0 +1,111 @@
1
+ module Ribbon
2
+ module EventBus
3
+ DEFAULT_CONFIG_PATH = File.expand_path('../../../../config/defaults.yml', __FILE__).freeze
4
+
5
+ ##############################################################################
6
+ # Instance
7
+ #
8
+ # Represents an instance of the EventBus. Allows multiple Instances to be
9
+ # created with separate configuration, subscriptions, etc. Primarily intended
10
+ # to help testing, but there are practical use-cases as well.
11
+ ##############################################################################
12
+ class Instance
13
+ include Mixins::Serializable
14
+
15
+ serialize_with :name
16
+
17
+ attr_reader :name
18
+ attr_reader :publishers
19
+
20
+ def initialize(name=nil)
21
+ if name
22
+ @name = name.to_sym
23
+ EventBus._register_instance(self) if @name
24
+ end
25
+
26
+ _load_default_config
27
+ end
28
+
29
+ def self.load_from_serialized(name)
30
+ if name
31
+ EventBus.instance(name)
32
+ else
33
+ raise Errors::InstanceError, "Can't deserialize an unnamed Instance"
34
+ end
35
+ end
36
+
37
+ def config(&block)
38
+ (@__config ||= Config.new).tap { |config|
39
+ if block_given?
40
+ config.define(&block)
41
+ _process_config
42
+ end
43
+ }
44
+ end
45
+
46
+ def publish(*args)
47
+ raise Errors::NoPublishersDefinedError unless publishers && !publishers.empty?
48
+ _args_to_event(*args).publish
49
+ end
50
+
51
+ def subscribe_to(event_name, params={}, &block)
52
+ Subscription.new(event_name, params.merge(instance: self), &block)
53
+ end
54
+
55
+ def subscriptions_to(event_or_name)
56
+ event_name = event_or_name.is_a?(Event) ? event_or_name.name : event_or_name.to_sym
57
+ _registered_subscriptions_to(event_name).dup
58
+ end
59
+
60
+ def find_subscription(locator)
61
+ _subscriptions_by_locators[locator]
62
+ end
63
+
64
+ def _register_subscription(subscription)
65
+ if _subscriptions_by_locators[subscription.locator]
66
+ # This is not expected to occur
67
+ raise Errors::SubscriptionError, "duplicate locator: #{subscription.locator.inspect}"
68
+ else
69
+ _subscriptions_by_locators[subscription.locator] = subscription
70
+ end
71
+
72
+ _registered_subscriptions_to(subscription.event_name)
73
+ .push(subscription)
74
+ .sort! { |x, y| x.priority <=> y.priority }
75
+ end
76
+
77
+ private
78
+ def _registered_subscriptions_to(event_name)
79
+ (@__registered_subscriptions ||= {})[event_name] ||= []
80
+ end
81
+
82
+ def _subscriptions_by_locators
83
+ @__registered_subscriptions_by_locator ||= {}
84
+ end
85
+
86
+ def _load_default_config
87
+ config.merge_config_file!(DEFAULT_CONFIG_PATH)
88
+ end
89
+
90
+ # Attempt to convert *args to an event object.
91
+ def _args_to_event(name_or_event, params={})
92
+ raise ArgumentError, 'event parameters must be a hash' unless params.is_a?(Hash)
93
+
94
+ case name_or_event
95
+ when Event
96
+ name_or_event.tap { |e| e.instance_variable_set(:@instance, self) }
97
+ else
98
+ Event.new(name_or_event, params.merge(instance: self))
99
+ end
100
+ end # _args_to_event
101
+
102
+ def _process_config
103
+ @publishers = _load_publishers.dup.freeze
104
+ end
105
+
106
+ def _load_publishers
107
+ Publishers.load_for_instance(self)
108
+ end
109
+ end # Instance
110
+ end # EventBus
111
+ end # Ribbon
@@ -0,0 +1,45 @@
1
+ module Ribbon::EventBus
2
+ module Mixins
3
+ module HasConfig
4
+ def self.included(base)
5
+ raise "HasConfig requires HasInstance" unless base < HasInstance
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def config_key(key)
11
+ config_keys << key.to_sym
12
+ end
13
+
14
+ def config_keys(*keys)
15
+ unless keys.empty?
16
+ _has_config_values[:keys] = keys.map(&:to_sym)
17
+ else
18
+ _has_config_values[:keys] ||= _has_config_ancestor_keys
19
+ end
20
+ end
21
+
22
+ def _has_config_ancestor_keys
23
+ ancestors[1] < HasConfig ? ancestors[1].config_keys.dup : []
24
+ end
25
+
26
+ def _has_config_values
27
+ @__has_config_values ||= {}
28
+ end
29
+ end
30
+
31
+ def config
32
+ _has_config_config
33
+ end
34
+
35
+ def _has_config_config
36
+ @__has_config_config ||= _has_config_load_config.dup
37
+ end
38
+
39
+ def _has_config_load_config
40
+ keys = self.class.config_keys
41
+ keys.inject(instance.config) { |c, k| c.send(k) }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ module Ribbon
2
+ module EventBus::Mixins
3
+ module HasInstance
4
+ def instance
5
+ (defined?(@instance) && @instance) || EventBus.instance
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,183 @@
1
+ require 'base64'
2
+
3
+ module Ribbon::EventBus
4
+ module Mixins
5
+ module Serializable
6
+ MAGIC = :SRLZ
7
+ VERSION = 1
8
+
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ def serialize_with(*args)
15
+ _serializable_values[:args] = args.map(&:to_sym)
16
+ end
17
+
18
+ def deserialize(encoded)
19
+ marshalled = _serializable_decode(encoded)
20
+ package = _serializable_unmarshal(marshalled)
21
+ _serializable_load_package(package)
22
+ end
23
+
24
+ def _serializable_load_package(package)
25
+ klass, args = _serializable_unpackage(package)
26
+ klass._deserialize_args(args)
27
+ end
28
+
29
+ def _deserialize_args(args)
30
+ args = args.map { |arg| _deserialize_arg(arg) }
31
+
32
+ if respond_to?(:load_from_serialized)
33
+ load_from_serialized(*args)
34
+ else
35
+ new(*args)
36
+ end
37
+ end
38
+
39
+ def _deserialize_arg(arg)
40
+ if _serializable_valid_package?(arg, false)
41
+ _serializable_load_package(arg)
42
+ else
43
+ arg
44
+ end
45
+ end
46
+
47
+ ###
48
+ # Encoding
49
+ ###
50
+ def _serializable_encode(marshalled)
51
+ Base64.strict_encode64(marshalled) # => encoded
52
+ end
53
+
54
+ def _serializable_decode(encoded)
55
+ begin
56
+ Base64.strict_decode64(encoded) # => marshalled
57
+ rescue ArgumentError
58
+ raise Errors::SerializableError, 'serialized string not encoded properly'
59
+ end
60
+ end
61
+
62
+ ###
63
+ # Marshalling
64
+ ###
65
+ def _serializable_marshal(package)
66
+ Marshal.dump(package) # => marshalled
67
+ end
68
+
69
+ def _serializable_unmarshal(marshalled)
70
+ begin
71
+ Marshal.load(marshalled) # => package
72
+ rescue TypeError
73
+ raise Errors::SerializableError, 'incorrect format'
74
+ end
75
+ end
76
+
77
+ ###
78
+ # Class Encoding
79
+ ###
80
+
81
+ def _serializable_encode_class(klass)
82
+ klass.name.to_s.sub('Ribbon::EventBus::', '').to_sym
83
+ end
84
+
85
+ def _serializable_decode_class(encoded_klass)
86
+ Ribbon::EventBus.const_get(encoded_klass.to_s)
87
+ end
88
+
89
+ ###
90
+ # Packaging
91
+ ###
92
+ def _serializable_package(klass, args)
93
+ encoded_klass = _serializable_encode_class(klass)
94
+ [MAGIC, VERSION, encoded_klass] + args # => package
95
+ end
96
+
97
+ def _serializable_valid_package?(package, noisy=true)
98
+ unless package.is_a?(Array)
99
+ if noisy
100
+ raise Errors::SerializableError, 'not a package'
101
+ else
102
+ return false
103
+ end
104
+ end
105
+
106
+ magic, version, class_name = package
107
+
108
+ # Check Magic
109
+ unless magic == MAGIC
110
+ if noisy
111
+ raise Errors::SerializableError, 'invalid serialized package'
112
+ else
113
+ return false
114
+ end
115
+ end
116
+
117
+ # Check Version
118
+ unless version == VERSION
119
+ if noisy
120
+ raise Errors::SerializableError, 'unsupported package version'
121
+ else
122
+ return false
123
+ end
124
+ end
125
+
126
+ # Check Class Name
127
+ unless class_name.is_a?(Symbol)
128
+ if noisy
129
+ raise Errors::SerializableError, 'invalid class name'
130
+ else
131
+ return false
132
+ end
133
+ end
134
+
135
+ return true
136
+ end
137
+
138
+ def _serializable_unpackage(package)
139
+ _serializable_valid_package?(package)
140
+ magic, version, encoded_klass, *args = package
141
+ klass = _serializable_decode_class(encoded_klass)
142
+ [klass, args]
143
+ end
144
+
145
+ ###
146
+ # Helpers
147
+ ###
148
+ def _serializable_values
149
+ @__serializable_values ||= {}
150
+ end
151
+
152
+ def _serializable_args
153
+ _serializable_values[:args] or
154
+ raise Errors::SerializableError, "Missing serialize_with definition"
155
+ end
156
+ end
157
+
158
+ def serialize
159
+ package = _serializable_package
160
+ marshalled = self.class._serializable_marshal(package)
161
+ self.class._serializable_encode(marshalled)
162
+ end
163
+
164
+ def _serializable_package
165
+ args = _serializable_args.map { |arg| _serialize_arg(send(arg)) }
166
+ self.class._serializable_package(self.class, args)
167
+ end
168
+
169
+ def _serialize_arg(arg)
170
+ case arg
171
+ when Serializable
172
+ arg._serializable_package
173
+ else
174
+ arg
175
+ end
176
+ end
177
+
178
+ def _serializable_args
179
+ self.class._serializable_args
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,7 @@
1
+ module Ribbon::EventBus
2
+ module Mixins
3
+ autoload(:HasInstance, 'ribbon/event_bus/mixins/has_instance')
4
+ autoload(:HasConfig, 'ribbon/event_bus/mixins/has_config')
5
+ autoload(:Serializable, 'ribbon/event_bus/mixins/serializable')
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ module Ribbon::EventBus
2
+ module Publishers
3
+ class ProcPublisher < Publisher
4
+ def initialize(instance=nil, &block)
5
+ super
6
+
7
+ raise Errors::MissingProcError unless block_given?
8
+ raise Errors::InvalidArityError, 'Proc arity must be 1' unless block.arity == 1
9
+ @_block = block
10
+ end
11
+
12
+ def new(instance=nil)
13
+ self.class.new(instance, &@_block)
14
+ end
15
+
16
+ def publish(event)
17
+ super
18
+ @_block.call(event)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ module Ribbon::EventBus
2
+ module Publishers
3
+ class Publisher
4
+ include Mixins::HasInstance
5
+ include Mixins::HasConfig
6
+ config_key :publishers
7
+
8
+ def initialize(instance=nil, params={})
9
+ @instance = instance
10
+ @_params = params
11
+ end
12
+
13
+ def config
14
+ @__config ||= super.merge_hash!(@_params)
15
+ end
16
+
17
+ ###
18
+ # #publish(event)
19
+ #
20
+ # This method should be overridden by a subclass. Make sure to call "super"
21
+ # so that proper sanity checks can be performed.
22
+ ###
23
+ def publish(event)
24
+ unless event.instance == instance
25
+ raise Errors::PublisherError, "Event for different instance"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,82 @@
1
+ require 'uri'
2
+ require 'redis'
3
+ require 'redis/namespace'
4
+
5
+ module Ribbon::EventBus
6
+ module Publishers
7
+ class RemoteResquePublisher < Publisher
8
+ config_key :remote_resque
9
+
10
+ def publish(event)
11
+ super
12
+
13
+ # Based on Resque 1.25.2
14
+
15
+ # Resque call stack:
16
+ # -> Resque.enqueue(klass, *args)
17
+ # -> Resque.enqueue_to(queue, klass, *args)
18
+ # -> Job.create(queue, klass, *args)
19
+ # -> Resque.push(queue, class: klass.to_s, args: args)
20
+
21
+ # These should be the same as the args passed to Resque.enqueue in
22
+ # ResquePublisher#publish(event).
23
+ args = [
24
+ config.subscription_queue_format.to_s,
25
+ event.serialize
26
+ ]
27
+
28
+ enqueue_to(config.queue.to_s, Publishers::ResquePublisher::PublisherJob, *args)
29
+ end
30
+
31
+ def redis
32
+ @redis ||= _redis
33
+ end
34
+
35
+ private
36
+
37
+ ##########################################################################
38
+ # Methods copied from Resque v1.25.2
39
+ ##########################################################################
40
+
41
+ def enqueue_to(queue, klass, *args)
42
+ # This is a functionality copy, not a direct code copy.
43
+ # Here, I'm skipping the call to Job.create(queue, klass, *args) and
44
+ # calling push directly.
45
+ push(queue, class: klass.to_s, args: args)
46
+ end
47
+
48
+ # Resque::push(queue, items)
49
+ def push(queue, item)
50
+ redis.pipelined do
51
+ watch_queue(queue)
52
+ redis.rpush "queue:#{queue}", encode(item)
53
+ end
54
+ end
55
+
56
+ # Resque::watch_queue
57
+ def watch_queue(queue)
58
+ redis.sadd(:queues, queue.to_s)
59
+ end
60
+
61
+ def encode(object)
62
+ # This one we can call directly.
63
+ Resque.encode(object)
64
+ end
65
+
66
+ ##########################################################################
67
+ # Helper Methods
68
+ ##########################################################################
69
+
70
+ def _redis
71
+ if config.redis?
72
+ config.redis
73
+ elsif config.redis_url?
74
+ redis = Redis.connect(url: config.redis_url, thread_safe: true)
75
+ Redis::Namespace.new(config.redis_namespace.to_sym, redis: redis)
76
+ else
77
+ raise Errors::RemoteResquePublisherError, "missing redis configuration"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,52 @@
1
+ require 'resque'
2
+
3
+ module Ribbon::EventBus
4
+ module Publishers
5
+ class ResquePublisher < Publisher
6
+ config_key :resque
7
+
8
+ def publish(event)
9
+ super
10
+
11
+ unless event.subscriptions.empty?
12
+ PublisherJob.set_queue(config.publisher_queue.to_sym)
13
+ sub_queue_format = config.subscription_queue_format.to_s
14
+ Resque.enqueue(PublisherJob, sub_queue_format, event.serialize)
15
+ end
16
+ end
17
+
18
+ module PublisherJob
19
+ def self.set_queue(queue)
20
+ @queue = queue
21
+ end
22
+
23
+ def self.perform(sub_queue_format, serialized_event)
24
+ event = Event.deserialize(serialized_event)
25
+
26
+ event.subscriptions.each { |s|
27
+ SubscriptionJob.set_queue(
28
+ (sub_queue_format % {
29
+ event: event.name,
30
+ priority: s.priority
31
+ }).to_sym
32
+ )
33
+
34
+ Resque.enqueue(SubscriptionJob, s.serialize, event.serialize)
35
+ }
36
+ end
37
+ end
38
+
39
+ module SubscriptionJob
40
+ def self.set_queue(queue)
41
+ @queue = queue
42
+ end
43
+
44
+ def self.perform(serialized_sub, serialized_event)
45
+ subscription = Subscription.deserialize(serialized_sub)
46
+ event = Event.deserialize(serialized_event)
47
+ subscription.handle(event)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ module Ribbon::EventBus::Publishers
2
+ class SubscriptionsPublisher < Publisher
3
+ def publish(event)
4
+ super
5
+ event.subscriptions.each { |subscription| subscription.handle(event) }
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,52 @@
1
+ module Ribbon::EventBus
2
+ module Publishers
3
+ autoload(:Publisher,
4
+ 'ribbon/event_bus/publishers/publisher')
5
+ autoload(:ProcPublisher,
6
+ 'ribbon/event_bus/publishers/proc_publisher')
7
+ autoload(:SubscriptionsPublisher,
8
+ 'ribbon/event_bus/publishers/subscriptions_publisher')
9
+ autoload(:ResquePublisher,
10
+ 'ribbon/event_bus/publishers/resque_publisher')
11
+ autoload(:RemoteResquePublisher,
12
+ 'ribbon/event_bus/publishers/remote_resque_publisher')
13
+
14
+ module_function
15
+ def load_for_instance(instance)
16
+ config = instance.config
17
+ config.publish_to? ? _load_for_instance(instance, config.publish_to) : []
18
+ end
19
+
20
+ def _load_for_instance(instance, publishers)
21
+ publishers.map { |publisher| _load_with_args(publisher, instance) }
22
+ end
23
+
24
+ def _load_with_args(publisher, *args)
25
+ case publisher
26
+ when Array
27
+ _load_with_args(publisher[0], *(args + publisher[1..-1]))
28
+ else
29
+ load(publisher).new(*args)
30
+ end
31
+ end
32
+
33
+ def load(publisher)
34
+ case publisher
35
+ when String, Symbol then _load_from_string(publisher.to_s)
36
+ when Proc then _load_from_proc(publisher)
37
+ when Publisher then publisher
38
+ else raise Errors::InvalidPublisherError, publisher.inspect
39
+ end
40
+ end
41
+
42
+ def _load_from_string(publisher_name)
43
+ const_get((publisher_name.split('_').map(&:capitalize) + ['Publisher']).join)
44
+ rescue NameError
45
+ raise Errors::InvalidPublisherNameError, publisher_name
46
+ end
47
+
48
+ def _load_from_proc(publisher_proc)
49
+ ProcPublisher.new(&publisher_proc)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,118 @@
1
+ require 'digest'
2
+
3
+ module Ribbon::EventBus
4
+ class Subscription
5
+ include Mixins::HasInstance
6
+ include Mixins::HasConfig
7
+ include Mixins::Serializable
8
+
9
+ config_key :subscriptions
10
+ serialize_with :instance, :locator
11
+
12
+ attr_reader :event_name
13
+ attr_reader :priority
14
+ attr_reader :locator
15
+
16
+ PRIORITY_SYMBOL_TO_INTEGER_MAP = {
17
+ highest: 1,
18
+ high: 3,
19
+ medium: 5,
20
+ low: 7,
21
+ lowest: 10
22
+ }.freeze
23
+
24
+ def initialize(event_name, params={}, &block)
25
+ @event_name = event_name.to_sym
26
+ @_block = block
27
+
28
+ _evaluate_params(params)
29
+ _generate_locator
30
+
31
+ instance._register_subscription(self)
32
+ end
33
+
34
+ def self.load_from_serialized(instance, locator)
35
+ instance.find_subscription(locator)
36
+ end
37
+
38
+ def handle(event)
39
+ raise Errors::UnexpectedEventError, 'wrong name' unless event.name == event_name
40
+ raise Errors::UnexpectedEventError, 'wrong instance' unless event.instance == instance
41
+ @_block.call(event)
42
+ end
43
+
44
+ private
45
+
46
+ def _generate_locator
47
+ path = File.expand_path('../..', __FILE__)
48
+ non_event_bus_caller = caller.find { |c| !c.start_with?(path) }
49
+
50
+ unless non_event_bus_caller
51
+ # This is not expected to occur.
52
+ raise Errors::SubscriptionError, "Could not find non-EventBus caller"
53
+ end
54
+
55
+ @locator = Digest::MD5.hexdigest(non_event_bus_caller).to_sym
56
+ end
57
+
58
+ ############################################################################
59
+ # Parameter Evaluation Logic
60
+ #
61
+ # This evaluates the parameters passed to the initializer.
62
+ ############################################################################
63
+
64
+ ###
65
+ # Root evaluation method.
66
+ ###
67
+ def _evaluate_params(params)
68
+ params = params.dup
69
+ @instance = params.delete(:instance)
70
+ @priority = _evaluate_priority(params.delete(:priority))
71
+ end
72
+
73
+ ###
74
+ # Priority evaluation
75
+ ###
76
+ def _evaluate_priority(priority)
77
+ case priority
78
+ when Integer
79
+ _evaluate_priority_int(priority)
80
+ when String, Symbol
81
+ _evaluate_priority_symbol(priority.to_sym)
82
+ when NilClass
83
+ _evaluate_priority_nil
84
+ else
85
+ raise Errors::InvalidPriorityError, priority.inspect
86
+ end
87
+ end
88
+
89
+ # Evaluate an integer as a priority.
90
+ def _evaluate_priority_int(int)
91
+ raise Errors::InvalidPriorityError, int unless int > 0 && int <= 10
92
+ int
93
+ end
94
+
95
+ # Evaluate a symbol as a priority.
96
+ def _evaluate_priority_symbol(sym)
97
+ if (priority = PRIORITY_SYMBOL_TO_INTEGER_MAP[sym])
98
+ _evaluate_priority(priority)
99
+ else
100
+ raise Errors::InvalidPriorityError, sym.inspect
101
+ end
102
+ end
103
+
104
+ # Evaluate nil as a priority.
105
+ def _evaluate_priority_nil
106
+ # Need to specify value explicitly here, otherwise in the call to
107
+ # _evaluate_priority, the case statement won't recognize it as a Symbol.
108
+ # That's because when calls Symbol::=== to evaluate a match.
109
+ priority = config.default_priority
110
+
111
+ if priority
112
+ _evaluate_priority(priority)
113
+ else
114
+ raise Errors::InvalidPriorityError, priority.inspect
115
+ end
116
+ end
117
+ end # Event
118
+ end # Ribbon::EventBus
@@ -0,0 +1,5 @@
1
+ module Ribbon
2
+ module EventBus
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,36 @@
1
+ module Ribbon
2
+ module EventBus
3
+ autoload(:Instance, 'ribbon/event_bus/instance')
4
+ autoload(:Errors, 'ribbon/event_bus/errors')
5
+ autoload(:Config, 'ribbon/event_bus/config')
6
+ autoload(:Event, 'ribbon/event_bus/event')
7
+ autoload(:Subscription, 'ribbon/event_bus/subscription')
8
+ autoload(:Publishers, 'ribbon/event_bus/publishers')
9
+ autoload(:Mixins, 'ribbon/event_bus/mixins')
10
+
11
+ module_function
12
+
13
+ def method_missing(meth, *args, &block)
14
+ instance.send(meth, *args, &block)
15
+ end
16
+
17
+ def instance(name=:primary)
18
+ _registered_instances[name.to_sym] || Instance.new(name)
19
+ end
20
+
21
+ def _registered_instances
22
+ @__registered_instances ||= {}
23
+ end
24
+
25
+ def _register_instance(instance)
26
+ if _registered_instances.key?(instance.name)
27
+ raise Errors::DuplicateInstanceNameError, instance.name
28
+ else
29
+ _registered_instances[instance.name] = instance
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ # Create a shortcut
36
+ EventBus = Ribbon::EventBus
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ribbon-event_bus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Robert Honer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-03-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.13
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.0.13
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: redis
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: redis-namespace
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: resque
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.25.2
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.25.2
83
+ - !ruby/object:Gem::Dependency
84
+ name: mock_redis
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec-rails
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: An asynchronous event bus for Ruby.
126
+ email:
127
+ - robert@ribbonpayments.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - config/defaults.yml
133
+ - lib/ribbon/event_bus.rb
134
+ - lib/ribbon/event_bus/config.rb
135
+ - lib/ribbon/event_bus/errors.rb
136
+ - lib/ribbon/event_bus/event.rb
137
+ - lib/ribbon/event_bus/instance.rb
138
+ - lib/ribbon/event_bus/mixins.rb
139
+ - lib/ribbon/event_bus/mixins/has_config.rb
140
+ - lib/ribbon/event_bus/mixins/has_instance.rb
141
+ - lib/ribbon/event_bus/mixins/serializable.rb
142
+ - lib/ribbon/event_bus/publishers.rb
143
+ - lib/ribbon/event_bus/publishers/proc_publisher.rb
144
+ - lib/ribbon/event_bus/publishers/publisher.rb
145
+ - lib/ribbon/event_bus/publishers/remote_resque_publisher.rb
146
+ - lib/ribbon/event_bus/publishers/resque_publisher.rb
147
+ - lib/ribbon/event_bus/publishers/subscriptions_publisher.rb
148
+ - lib/ribbon/event_bus/subscription.rb
149
+ - lib/ribbon/event_bus/version.rb
150
+ homepage: http://github.com/ribbon/event_bus
151
+ licenses: []
152
+ metadata: {}
153
+ post_install_message:
154
+ rdoc_options: []
155
+ require_paths:
156
+ - lib
157
+ required_ruby_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ required_rubygems_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ requirements: []
168
+ rubyforge_project:
169
+ rubygems_version: 2.4.5
170
+ signing_key:
171
+ specification_version: 4
172
+ summary: An asynchronous event bus for Ruby.
173
+ test_files: []