announcer 0.5.1

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.
@@ -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: []