announcer 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,81 @@
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
+ event.serialize
25
+ ]
26
+
27
+ enqueue_to(config.queue.to_s, Publishers::ResquePublisher::PublisherJob, *args)
28
+ end
29
+
30
+ def redis
31
+ @redis ||= _redis
32
+ end
33
+
34
+ private
35
+
36
+ ##########################################################################
37
+ # Methods copied from Resque v1.25.2
38
+ ##########################################################################
39
+
40
+ def enqueue_to(queue, klass, *args)
41
+ # This is a functionality copy, not a direct code copy.
42
+ # Here, I'm skipping the call to Job.create(queue, klass, *args) and
43
+ # calling push directly.
44
+ push(queue, class: klass.to_s, args: args)
45
+ end
46
+
47
+ # Resque::push(queue, items)
48
+ def push(queue, item)
49
+ redis.pipelined do
50
+ watch_queue(queue)
51
+ redis.rpush "queue:#{queue}", encode(item)
52
+ end
53
+ end
54
+
55
+ # Resque::watch_queue
56
+ def watch_queue(queue)
57
+ redis.sadd(:queues, queue.to_s)
58
+ end
59
+
60
+ def encode(object)
61
+ # This one we can call directly.
62
+ Resque.encode(object)
63
+ end
64
+
65
+ ##########################################################################
66
+ # Helper Methods
67
+ ##########################################################################
68
+
69
+ def _redis
70
+ if config.redis?
71
+ config.redis
72
+ elsif config.redis_url?
73
+ redis = Redis.connect(url: config.redis_url, thread_safe: true)
74
+ Redis::Namespace.new(config.redis_namespace.to_sym, redis: redis)
75
+ else
76
+ raise Errors::RemoteResquePublisherError, "missing redis configuration"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,87 @@
1
+ require 'resque'
2
+
3
+ module Ribbon::EventBus
4
+ module Publishers
5
+ class ResquePublisher < Publisher
6
+ config_key :resque
7
+
8
+ def initialize(instance=nil, params={})
9
+ super
10
+ _disallow_multiple_per_instance
11
+ end
12
+
13
+ def publish(event)
14
+ super
15
+
16
+ unless event.subscriptions.empty?
17
+ PublisherJob.set_queue(config.publisher_queue.to_sym)
18
+ Resque.enqueue(PublisherJob, event.serialize)
19
+ end
20
+ end
21
+
22
+ def subscription_queue_formatter
23
+ self.class.subscription_queue_formatter(config)
24
+ end
25
+
26
+ class << self
27
+ def subscription_queue_formatter(config)
28
+ if config.subscription_queue_formatter?
29
+ formatter = config.subscription_queue_formatter
30
+ case formatter
31
+ when Array
32
+ formatter.last
33
+ when Proc
34
+ formatter
35
+ else
36
+ raise Errors::PublisherError, "Invalid subscription_queue_formatter: #{formatter.inspect}"
37
+ end
38
+ else
39
+ lambda { |subscription| "subscriptions_p#{subscription.priority}" }
40
+ end
41
+ end
42
+ end # Class Methods
43
+
44
+ module PublisherJob
45
+ def self.set_queue(queue)
46
+ @queue = queue
47
+ end
48
+
49
+ def self.perform(serialized_event, publisher_name=:resque)
50
+ event = Event.deserialize(serialized_event)
51
+ instance = event.instance
52
+
53
+ publisher = instance.find_publisher(publisher_name)
54
+ raise Errors::PublisherError, 'No ResquePublisher found' unless publisher
55
+ queue_formatter = publisher.subscription_queue_formatter
56
+
57
+ instance.plugins.perform(:resque_publish, event) do |event|
58
+ event.subscriptions.each { |s|
59
+ SubscriptionJob.set_queue(queue_formatter.call(s).to_sym)
60
+ Resque.enqueue(SubscriptionJob, s.serialize, event.serialize)
61
+ }
62
+ end
63
+ end
64
+ end # PublisherJob
65
+
66
+ module SubscriptionJob
67
+ def self.set_queue(queue)
68
+ @queue = queue
69
+ end
70
+
71
+ def self.perform(serialized_sub, serialized_event)
72
+ subscription = Subscription.deserialize(serialized_sub)
73
+ event = Event.deserialize(serialized_event)
74
+ subscription.handle(event)
75
+ end
76
+ end # SubscriptionJob
77
+
78
+ private
79
+ def _disallow_multiple_per_instance
80
+ if instance.has_publisher?(:resque)
81
+ raise Errors::PublisherError,
82
+ "cannot have multiple ResquePublishers in an EventBus instance"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ 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,165 @@
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, :identifier
11
+
12
+ attr_reader :name
13
+ attr_reader :event_name
14
+ attr_reader :priority
15
+ attr_reader :identifier
16
+
17
+ def initialize(event_name, params={}, &block)
18
+ @event_name = event_name.to_sym
19
+ @_block = block
20
+
21
+ _evaluate_params(params)
22
+
23
+ @identifier = _generate_identifier
24
+
25
+ if instance.find_subscription(identifier)
26
+ raise Errors::DuplicateIdentifierError, "give this subscription a unique name"
27
+ else
28
+ instance._register_subscription(self)
29
+ end
30
+ end
31
+
32
+ def self.load_from_serialized(instance, identifier)
33
+ instance.find_subscription(identifier)
34
+ end
35
+
36
+ def handle(event)
37
+ raise Errors::UnexpectedEventError, 'wrong name' unless event.name == event_name
38
+ raise Errors::UnexpectedEventError, 'wrong instance' unless event.instance == instance
39
+
40
+ plugins.perform(:subscription, self, event) { |subscription, event|
41
+ @_block.call(event)
42
+ }
43
+ end
44
+
45
+ def to_s
46
+ "Subscription(on #{event_name}: #{name || _path})"
47
+ end
48
+
49
+ private
50
+
51
+ def _path
52
+ @__path ||= _determine_path
53
+ end
54
+
55
+ ##
56
+ # Determines the file path of the ruby code defining the subscription.
57
+ # It's important that this is called from within the initializer to get the
58
+ # desired effect.
59
+ def _determine_path
60
+ path = File.expand_path('../..', __FILE__)
61
+
62
+ # Will be something like:
63
+ # "/path/to/file.rb:47:in `method_name'"
64
+ non_event_bus_caller = caller.find { |c| !c.start_with?(path) }
65
+
66
+ unless non_event_bus_caller
67
+ # This is not expected to occur.
68
+ raise Errors::SubscriptionError, "Could not find non-EventBus caller"
69
+ end
70
+
71
+ non_event_bus_caller
72
+ end
73
+
74
+ ##
75
+ # Generates a unique identifier for this subscription which will be used to
76
+ # "serialize" it.
77
+ #
78
+ # The goal here is to generate a identifier that is the same across different
79
+ # processes and servers and that ideally changes infrequently between
80
+ # application versions, but when it does change, it should do so predictably.
81
+ def _generate_identifier
82
+ # Cut off everything from the line number onward. That way, the identifier
83
+ # does not change when the subscription block moves to a different line.
84
+ index = _path.rindex(/:\d+:/) - 1
85
+ path = _path[0..index]
86
+
87
+ raise Errors::SubscriptionError, "Invalid path: #{path}" unless File.exists?(path)
88
+
89
+ Digest::MD5.hexdigest("#{path}:#{event_name}:#{name}").to_sym
90
+ end
91
+
92
+ def _symbol_to_priority(sym)
93
+ (@__symbol_to_priority_map ||= _generate_priority_shortcut_map)[sym]
94
+ end
95
+
96
+ def _generate_priority_shortcut_map(max_priority=config.max_priority)
97
+ {}.tap { |map|
98
+ map.merge!(highest: 1, lowest: max_priority)
99
+ map[:medium] = (map[:lowest] / 2.0).ceil
100
+ map[:high] = (map[:medium] / 2.0).ceil
101
+ map[:low] = ((map[:lowest] + map[:medium]) / 2.0).ceil
102
+ }.freeze
103
+ end
104
+
105
+ ############################################################################
106
+ # Parameter Evaluation Logic
107
+ #
108
+ # This evaluates the parameters passed to the initializer.
109
+ ############################################################################
110
+
111
+ ###
112
+ # Root evaluation method.
113
+ ###
114
+ def _evaluate_params(params)
115
+ @instance = params[:instance]
116
+ @name = params[:name].to_s
117
+ @priority = _evaluate_priority(params[:priority])
118
+ end
119
+
120
+ ###
121
+ # Priority evaluation
122
+ ###
123
+ def _evaluate_priority(priority)
124
+ case priority
125
+ when Integer
126
+ _evaluate_priority_int(priority)
127
+ when String, Symbol
128
+ _evaluate_priority_symbol(priority.to_sym)
129
+ when NilClass
130
+ _evaluate_priority_nil
131
+ else
132
+ raise Errors::InvalidPriorityError, priority.inspect
133
+ end
134
+ end
135
+
136
+ # Evaluate an integer as a priority.
137
+ def _evaluate_priority_int(int)
138
+ raise Errors::InvalidPriorityError, int unless int > 0 && int <= config.max_priority
139
+ int
140
+ end
141
+
142
+ # Evaluate a symbol as a priority.
143
+ def _evaluate_priority_symbol(sym)
144
+ if (priority = _symbol_to_priority(sym))
145
+ _evaluate_priority(priority)
146
+ else
147
+ raise Errors::InvalidPriorityError, sym.inspect
148
+ end
149
+ end
150
+
151
+ # Evaluate nil as a priority.
152
+ def _evaluate_priority_nil
153
+ # Need to specify value explicitly here, otherwise in the call to
154
+ # _evaluate_priority, the case statement won't recognize it as a Symbol.
155
+ # That's because when calls Symbol::=== to evaluate a match.
156
+ priority = config.default_priority
157
+
158
+ if priority
159
+ _evaluate_priority(priority)
160
+ else
161
+ raise Errors::InvalidPriorityError, priority.inspect
162
+ end
163
+ end
164
+ end # Event
165
+ end # Ribbon::EventBus
@@ -0,0 +1,5 @@
1
+ module Ribbon
2
+ module EventBus
3
+ VERSION = "0.5.1"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: announcer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.1
5
+ platform: ruby
6
+ authors:
7
+ - Robert Honer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ribbon-plugins
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.2'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.2.4
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.2'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.2.4
33
+ - !ruby/object:Gem::Dependency
34
+ name: celluloid
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.17.2
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.17.2
47
+ - !ruby/object:Gem::Dependency
48
+ name: rails
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 4.0.13
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 4.0.13
61
+ - !ruby/object:Gem::Dependency
62
+ name: sqlite3
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: redis
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: redis-namespace
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: resque
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 1.25.2
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 1.25.2
117
+ - !ruby/object:Gem::Dependency
118
+ name: mock_redis
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: rspec
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ - !ruby/object:Gem::Dependency
146
+ name: rspec-rails
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ description: An asynchronous event bus for Ruby.
160
+ email:
161
+ - robert@ribbonpayments.com
162
+ executables: []
163
+ extensions: []
164
+ extra_rdoc_files: []
165
+ files:
166
+ - config/defaults.yml
167
+ - lib/ribbon/event_bus.rb
168
+ - lib/ribbon/event_bus/config.rb
169
+ - lib/ribbon/event_bus/errors.rb
170
+ - lib/ribbon/event_bus/event.rb
171
+ - lib/ribbon/event_bus/instance.rb
172
+ - lib/ribbon/event_bus/mixins.rb
173
+ - lib/ribbon/event_bus/mixins/has_config.rb
174
+ - lib/ribbon/event_bus/mixins/has_instance.rb
175
+ - lib/ribbon/event_bus/mixins/serializable.rb
176
+ - lib/ribbon/event_bus/plugins.rb
177
+ - lib/ribbon/event_bus/plugins/logging_plugin.rb
178
+ - lib/ribbon/event_bus/plugins/plugin.rb
179
+ - lib/ribbon/event_bus/publishers.rb
180
+ - lib/ribbon/event_bus/publishers/async_resque_publisher.rb
181
+ - lib/ribbon/event_bus/publishers/proc_publisher.rb
182
+ - lib/ribbon/event_bus/publishers/publisher.rb
183
+ - lib/ribbon/event_bus/publishers/remote_resque_publisher.rb
184
+ - lib/ribbon/event_bus/publishers/resque_publisher.rb
185
+ - lib/ribbon/event_bus/publishers/subscriptions_publisher.rb
186
+ - lib/ribbon/event_bus/subscription.rb
187
+ - lib/ribbon/event_bus/version.rb
188
+ homepage: http://github.com/ribbon/event_bus
189
+ licenses:
190
+ - BSD
191
+ metadata: {}
192
+ post_install_message:
193
+ rdoc_options: []
194
+ require_paths:
195
+ - lib
196
+ required_ruby_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ required_rubygems_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ requirements: []
207
+ rubyforge_project:
208
+ rubygems_version: 2.4.3
209
+ signing_key:
210
+ specification_version: 4
211
+ summary: An asynchronous event bus for Ruby.
212
+ test_files: []