ribbon-event_bus 0.1.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.
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: []