tusk 1.0.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.
- data/.autotest +8 -0
- data/.gemtest +0 -0
- data/CHANGELOG.rdoc +6 -0
- data/Manifest.txt +13 -0
- data/README.markdown +206 -0
- data/Rakefile +20 -0
- data/lib/tusk.rb +9 -0
- data/lib/tusk/latch.rb +24 -0
- data/lib/tusk/observable/pg.rb +170 -0
- data/lib/tusk/observable/redis.rb +170 -0
- data/test/helper.rb +149 -0
- data/test/observable/test_pg.rb +57 -0
- data/test/observable/test_redis.rb +84 -0
- data/test/redis-test.conf +1 -0
- metadata +148 -0
data/.autotest
ADDED
data/.gemtest
ADDED
File without changes
|
data/CHANGELOG.rdoc
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
.autotest
|
2
|
+
CHANGELOG.rdoc
|
3
|
+
Manifest.txt
|
4
|
+
README.markdown
|
5
|
+
Rakefile
|
6
|
+
lib/tusk.rb
|
7
|
+
lib/tusk/latch.rb
|
8
|
+
lib/tusk/observable/pg.rb
|
9
|
+
lib/tusk/observable/redis.rb
|
10
|
+
test/helper.rb
|
11
|
+
test/observable/test_pg.rb
|
12
|
+
test/observable/test_redis.rb
|
13
|
+
test/redis-test.conf
|
data/README.markdown
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
# tusk
|
2
|
+
|
3
|
+
* http://github.com/tenderlove/tusk
|
4
|
+
|
5
|
+
## DESCRIPTION:
|
6
|
+
|
7
|
+
Tusk is a minimal pub / sub system with multiple observer strategies.
|
8
|
+
Tusk builds upon the Observer API from stdlib in order to provide a mostly
|
9
|
+
consistent API for building cross thread or process pub / sub systems.
|
10
|
+
|
11
|
+
Currently, Tusk supports Redis and PostgreSQL as message bus back ends.
|
12
|
+
|
13
|
+
## FEATURES/PROBLEMS:
|
14
|
+
|
15
|
+
* Send message across processes
|
16
|
+
* Supports Redis as a message bus
|
17
|
+
* Supports PostgreSQL as a message bus
|
18
|
+
|
19
|
+
## SYNOPSIS:
|
20
|
+
|
21
|
+
Here is an in-memory observer example:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
require 'observer'
|
25
|
+
|
26
|
+
class Timer
|
27
|
+
include Observable
|
28
|
+
|
29
|
+
def tick
|
30
|
+
changed
|
31
|
+
notify_observers
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Listener
|
36
|
+
def update; puts "got update"; end
|
37
|
+
end
|
38
|
+
|
39
|
+
timer = Timer.new
|
40
|
+
timer.add_observer Listener.new
|
41
|
+
loop { timer.tick; sleep 1; }
|
42
|
+
```
|
43
|
+
|
44
|
+
|
45
|
+
The down side of this example is that the Listener cannot be in a different
|
46
|
+
process. We can move the Listener to a different process by using the Redis
|
47
|
+
observable mixin and providing a redis connection:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
require 'tusk/observable/redis'
|
51
|
+
require 'redis'
|
52
|
+
|
53
|
+
class Timer
|
54
|
+
include Tusk::Observable::Redis
|
55
|
+
|
56
|
+
def tick
|
57
|
+
changed
|
58
|
+
notify_observers
|
59
|
+
end
|
60
|
+
|
61
|
+
def connection
|
62
|
+
Thread.current[:redis] ||= ::Redis.new
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Listener
|
67
|
+
def update; puts "got update PID: #{$$}"; end
|
68
|
+
end
|
69
|
+
|
70
|
+
timer = Timer.new
|
71
|
+
|
72
|
+
fork {
|
73
|
+
timer.add_observer Listener.new
|
74
|
+
sleep
|
75
|
+
}
|
76
|
+
|
77
|
+
loop { timer.tick; sleep 1; }
|
78
|
+
```
|
79
|
+
|
80
|
+
PostgreSQL can also be used as the message bus:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
require 'tusk/observable/pg'
|
84
|
+
require 'pg'
|
85
|
+
|
86
|
+
class Timer
|
87
|
+
include Tusk::Observable::PG
|
88
|
+
|
89
|
+
def tick
|
90
|
+
changed
|
91
|
+
notify_observers
|
92
|
+
end
|
93
|
+
|
94
|
+
def connection
|
95
|
+
Thread.current[:pg] ||= ::PG::Connection.new :dbname => 'postgres'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class Listener
|
100
|
+
def update; puts "got update PID: #{$$}"; end
|
101
|
+
end
|
102
|
+
|
103
|
+
timer = Timer.new
|
104
|
+
|
105
|
+
fork {
|
106
|
+
timer.add_observer Listener.new
|
107
|
+
sleep
|
108
|
+
}
|
109
|
+
|
110
|
+
loop { timer.tick; sleep 1; }
|
111
|
+
```
|
112
|
+
|
113
|
+
We can easily integrate Tusk with Active Record. Here is a User model that
|
114
|
+
sends notifications when a user is created:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
require 'tusk/observable/pg'
|
118
|
+
class User < ActiveRecord::Base
|
119
|
+
attr_accessible :name
|
120
|
+
|
121
|
+
extend Tusk::Observable::PG
|
122
|
+
|
123
|
+
# After users are created, notify the message bus
|
124
|
+
after_create :notify_observers
|
125
|
+
|
126
|
+
# Listeners will use the table name as the bus channel
|
127
|
+
def self.channel
|
128
|
+
table_name
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def notify_observers
|
134
|
+
self.class.changed
|
135
|
+
self.class.notify_observers
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
The table name is used as the channel name where objects will listen. Here is
|
141
|
+
a producer script:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
require 'user'
|
145
|
+
loop do
|
146
|
+
User.create!(:name => 'testing')
|
147
|
+
sleep 1
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
Our consumer adds an observer to the User class:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
require 'user'
|
155
|
+
class UserListener
|
156
|
+
def initialize
|
157
|
+
super
|
158
|
+
@last_id = 0
|
159
|
+
end
|
160
|
+
|
161
|
+
def update
|
162
|
+
users = User.where('id > ?', @last_id).sort_by(&:id)
|
163
|
+
@last_id = users.last.id
|
164
|
+
users.each { |u| p "user created: #{u.id}" }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
User.add_observer UserListener.new
|
169
|
+
# Put the main thread to sleep
|
170
|
+
sleep
|
171
|
+
```
|
172
|
+
|
173
|
+
Whenever a user gets created, our consumer listener will be notified.
|
174
|
+
|
175
|
+
## REQUIREMENTS:
|
176
|
+
|
177
|
+
* PostgreSQL or Redis
|
178
|
+
|
179
|
+
## INSTALL:
|
180
|
+
|
181
|
+
* gem install tusk
|
182
|
+
|
183
|
+
## LICENSE:
|
184
|
+
|
185
|
+
(The MIT License)
|
186
|
+
|
187
|
+
Copyright (c) 2012 Aaron Patterson
|
188
|
+
|
189
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
190
|
+
a copy of this software and associated documentation files (the
|
191
|
+
'Software'), to deal in the Software without restriction, including
|
192
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
193
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
194
|
+
permit persons to whom the Software is furnished to do so, subject to
|
195
|
+
the following conditions:
|
196
|
+
|
197
|
+
The above copyright notice and this permission notice shall be
|
198
|
+
included in all copies or substantial portions of the Software.
|
199
|
+
|
200
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
201
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
202
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
203
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
204
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
205
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
206
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
|
6
|
+
Hoe.plugins.delete :rubyforge
|
7
|
+
Hoe.plugin :minitest
|
8
|
+
Hoe.plugin :gemspec # `gem install hoe-gemspec`
|
9
|
+
Hoe.plugin :git # `gem install hoe-git`
|
10
|
+
|
11
|
+
Hoe.spec 'tusk' do
|
12
|
+
developer('Aaron Patterson', 'aaron@tenderlovemaking.com')
|
13
|
+
self.readme_file = 'README.markdown'
|
14
|
+
self.history_file = 'CHANGELOG.rdoc'
|
15
|
+
self.extra_rdoc_files = FileList['*.{rdoc,markdown}']
|
16
|
+
|
17
|
+
self.extra_dev_deps << ['pg', '~> 0.14.0']
|
18
|
+
end
|
19
|
+
|
20
|
+
# vim: syntax=ruby
|
data/lib/tusk.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
###
|
2
|
+
# Tusk contains observers with different message bus strategies.
|
3
|
+
#
|
4
|
+
# Tusk::Observers::Redis offers an Observer API with Redis as the
|
5
|
+
# message bus. Tusk::Observers::PG offers and Observer API with
|
6
|
+
# PostgreSQL as the message bus.
|
7
|
+
module Tusk
|
8
|
+
VERSION = '1.0.0'
|
9
|
+
end
|
data/lib/tusk/latch.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
3
|
+
module Tusk
|
4
|
+
class Latch
|
5
|
+
def initialize(count = 1)
|
6
|
+
@count = count
|
7
|
+
@lock = Monitor.new
|
8
|
+
@cv = @lock.new_cond
|
9
|
+
end
|
10
|
+
|
11
|
+
def release
|
12
|
+
@lock.synchronize do
|
13
|
+
@count -= 1 if @count > 0
|
14
|
+
@cv.broadcast if @count.zero?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def await
|
19
|
+
@lock.synchronize do
|
20
|
+
@cv.wait_while { @count > 0 }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'tusk/latch'
|
4
|
+
|
5
|
+
module Tusk
|
6
|
+
module Observable
|
7
|
+
###
|
8
|
+
# An observer implementation for PostgreSQL. This module requires that
|
9
|
+
# your class implement a `connection` method that returns a database
|
10
|
+
# connection that this module can use.
|
11
|
+
#
|
12
|
+
# This observer works across processes.
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
#
|
16
|
+
# require 'pg'
|
17
|
+
# require 'tusk/observable/pg'
|
18
|
+
#
|
19
|
+
# class Timer
|
20
|
+
# include Tusk::Observable::PG
|
21
|
+
#
|
22
|
+
# def tick
|
23
|
+
# changed
|
24
|
+
# notify_observers
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# def connection
|
28
|
+
# Thread.current[:conn] ||= ::PG::Connection.new :dbname => 'postgres'
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# class Listener
|
33
|
+
# def update
|
34
|
+
# puts "got update"
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# timer = Timer.new
|
39
|
+
#
|
40
|
+
# fork do
|
41
|
+
# timer.add_observer Listener.new
|
42
|
+
# sleep # put the process to sleep so it doesn't exit
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# loop do
|
46
|
+
# timer.tick
|
47
|
+
# sleep 1
|
48
|
+
# end
|
49
|
+
module PG
|
50
|
+
def self.extended klass
|
51
|
+
super
|
52
|
+
|
53
|
+
klass.instance_eval do
|
54
|
+
@sub_lock = Mutex.new
|
55
|
+
@observer_state = false
|
56
|
+
@subscribers = {}
|
57
|
+
@_listener = nil
|
58
|
+
@observing = Latch.new
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
attr_reader :subscribers
|
63
|
+
|
64
|
+
def initialize *args
|
65
|
+
super
|
66
|
+
|
67
|
+
@sub_lock = Mutex.new
|
68
|
+
@observer_state = false
|
69
|
+
@subscribers = {}
|
70
|
+
@_listener = nil
|
71
|
+
@observing = Latch.new
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the number of observers associated with this object *in the
|
75
|
+
# current process*. If the object is observed across multiple processes,
|
76
|
+
# the returned count will not reflect the other processes.
|
77
|
+
def count_observers
|
78
|
+
@sub_lock.synchronize { subscribers.fetch(channel, {}).length }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Remove all observers associated with this object *in the current
|
82
|
+
# process*. This method will not impact observers of this object in
|
83
|
+
# other processes.
|
84
|
+
def delete_observers
|
85
|
+
@sub_lock.synchronize { subscribers.delete channel }
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns true if this object's state has been changed since the last
|
89
|
+
# call to #notify_observers.
|
90
|
+
def changed?
|
91
|
+
@observer_state
|
92
|
+
end
|
93
|
+
|
94
|
+
# Set the changed state of this object. Notifications will be sent only
|
95
|
+
# if the changed +state+ is a truthy object.
|
96
|
+
def changed state = true
|
97
|
+
@observer_state = state
|
98
|
+
end
|
99
|
+
|
100
|
+
# If this object's #changed? state is true, this method will notify
|
101
|
+
# observing objects.
|
102
|
+
def notify_observers
|
103
|
+
return unless changed?
|
104
|
+
|
105
|
+
unwrap(connection).exec "NOTIFY #{channel}"
|
106
|
+
|
107
|
+
changed false
|
108
|
+
end
|
109
|
+
|
110
|
+
# Add +observer+ as an observer to this object. The +object+ will
|
111
|
+
# receive a notification when #changed? returns true and #notify_observers
|
112
|
+
# is called.
|
113
|
+
#
|
114
|
+
# +func+ method is called on +object+ when notifications are sent.
|
115
|
+
def add_observer object, func = :update
|
116
|
+
@sub_lock.synchronize do
|
117
|
+
subscribers.fetch(channel) { |k|
|
118
|
+
Thread.new {
|
119
|
+
start_listener
|
120
|
+
unwrap(connection).exec "LISTEN #{channel}"
|
121
|
+
@observing.release
|
122
|
+
}
|
123
|
+
subscribers[k] = {}
|
124
|
+
}[object] = func
|
125
|
+
end
|
126
|
+
|
127
|
+
@observing.await
|
128
|
+
end
|
129
|
+
|
130
|
+
# Remove +observer+ so that it will no longer receive notifications.
|
131
|
+
def delete_observer o
|
132
|
+
@sub_lock.synchronize do
|
133
|
+
subscribers.fetch(channel, {}).delete o
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def unwrap conn
|
140
|
+
if conn.respond_to? :exec
|
141
|
+
conn
|
142
|
+
else
|
143
|
+
# Yes, I am a terrible person. This pulls
|
144
|
+
# the connection out of AR connections.
|
145
|
+
conn.instance_eval { @connection }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def channel
|
150
|
+
"a" + Digest::MD5.hexdigest("#{self.class.name}#{object_id}")
|
151
|
+
end
|
152
|
+
|
153
|
+
def start_listener
|
154
|
+
return if @_listener
|
155
|
+
|
156
|
+
@_listener = Thread.new(unwrap(connection)) do |conn|
|
157
|
+
@observing.release
|
158
|
+
|
159
|
+
loop do
|
160
|
+
conn.wait_for_notify do |event, pid|
|
161
|
+
subscribers.fetch(event, []).dup.each do |listener, func|
|
162
|
+
listener.send func
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'thread'
|
3
|
+
require 'tusk/latch'
|
4
|
+
|
5
|
+
module Tusk
|
6
|
+
module Observable
|
7
|
+
###
|
8
|
+
# An observer implementation for Redis. This module requires that
|
9
|
+
# your class implement a `connection` method that returns a redis
|
10
|
+
# connection that this module can use.
|
11
|
+
#
|
12
|
+
# This observer works across processes.
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
#
|
16
|
+
# require 'redis'
|
17
|
+
# require 'tusk/observable/redis'
|
18
|
+
#
|
19
|
+
# class Timer
|
20
|
+
# include Tusk::Observable::Redis
|
21
|
+
#
|
22
|
+
# def tick
|
23
|
+
# changed
|
24
|
+
# notify_observers
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# def connection
|
28
|
+
# Thread.current[:conn] ||= ::Redis.new
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# class Listener
|
33
|
+
# def update
|
34
|
+
# puts "got update"
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# timer = Timer.new
|
39
|
+
#
|
40
|
+
# fork do
|
41
|
+
# timer.add_observer Listener.new
|
42
|
+
# sleep # put the process to sleep so it doesn't exit
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# loop do
|
46
|
+
# timer.tick
|
47
|
+
# sleep 1
|
48
|
+
# end
|
49
|
+
module Redis
|
50
|
+
def self.extended klass
|
51
|
+
super
|
52
|
+
|
53
|
+
klass.instance_eval do
|
54
|
+
@sub_lock = Mutex.new
|
55
|
+
@observer_state = false
|
56
|
+
@subscribers = {}
|
57
|
+
@_listener = nil
|
58
|
+
@control_channel = SecureRandom.hex
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
attr_reader :subscribers, :control_channel
|
63
|
+
|
64
|
+
def initialize *args
|
65
|
+
super
|
66
|
+
|
67
|
+
@sub_lock = Mutex.new
|
68
|
+
@observer_state = false
|
69
|
+
@subscribers = {}
|
70
|
+
@_listener = nil
|
71
|
+
@control_channel = SecureRandom.hex
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the number of observers associated with this object *in the
|
75
|
+
# current process*. If the object is observed across multiple processes,
|
76
|
+
# the returned count will not reflect the other processes.
|
77
|
+
def count_observers
|
78
|
+
@sub_lock.synchronize { subscribers.fetch(channel, {}).length }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Remove all observers associated with this object *in the current
|
82
|
+
# process*. This method will not impact observers of this object in
|
83
|
+
# other processes.
|
84
|
+
def delete_observers
|
85
|
+
@sub_lock.synchronize { subscribers.delete channel }
|
86
|
+
connection.publish control_channel, 'quit'
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns true if this object's state has been changed since the last
|
90
|
+
# call to #notify_observers.
|
91
|
+
def changed?
|
92
|
+
@observer_state
|
93
|
+
end
|
94
|
+
|
95
|
+
# Set the changed state of this object. Notifications will be sent only
|
96
|
+
# if the changed +state+ is a truthy object.
|
97
|
+
def changed state = true
|
98
|
+
@observer_state = state
|
99
|
+
end
|
100
|
+
|
101
|
+
# If this object's #changed? state is true, this method will notify
|
102
|
+
# observing objects.
|
103
|
+
def notify_observers
|
104
|
+
return unless changed?
|
105
|
+
connection.publish channel, nil
|
106
|
+
changed false
|
107
|
+
end
|
108
|
+
|
109
|
+
# Add +observer+ as an observer to this object. The +object+ will
|
110
|
+
# receive a notification when #changed? returns true and #notify_observers
|
111
|
+
# is called.
|
112
|
+
#
|
113
|
+
# +func+ method is called on +object+ when notifications are sent.
|
114
|
+
def add_observer object, func = :update
|
115
|
+
observer_set = Latch.new
|
116
|
+
observing = Latch.new
|
117
|
+
|
118
|
+
@sub_lock.synchronize do
|
119
|
+
observing.release if subscribers.key? channel
|
120
|
+
|
121
|
+
subscribers.fetch(channel) { |k|
|
122
|
+
Thread.new {
|
123
|
+
observer_set.await
|
124
|
+
start_listener(observing)
|
125
|
+
}
|
126
|
+
subscribers[k] = {}
|
127
|
+
}[object] = func
|
128
|
+
end
|
129
|
+
|
130
|
+
observer_set.release
|
131
|
+
observing.await
|
132
|
+
end
|
133
|
+
|
134
|
+
# Remove +observer+ so that it will no longer receive notifications.
|
135
|
+
def delete_observer o
|
136
|
+
@sub_lock.synchronize do
|
137
|
+
subscribers.fetch(channel, {}).delete o
|
138
|
+
if subscribers.fetch(channel,{}).empty?
|
139
|
+
subscribers.delete channel
|
140
|
+
connection.publish control_channel, 'quit'
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def channel
|
148
|
+
"a" + Digest::MD5.hexdigest("#{self.class.name}#{object_id}")
|
149
|
+
end
|
150
|
+
|
151
|
+
def start_listener latch
|
152
|
+
connection.subscribe(channel, control_channel) do |on|
|
153
|
+
on.subscribe { |c| latch.release }
|
154
|
+
|
155
|
+
on.message do |c, message|
|
156
|
+
if c == control_channel && message == 'quit'
|
157
|
+
connection.unsubscribe
|
158
|
+
else
|
159
|
+
@sub_lock.synchronize do
|
160
|
+
subscribers.fetch(c, {}).each do |object,m|
|
161
|
+
object.send m
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
|
3
|
+
module Tusk
|
4
|
+
class TestCase < MiniTest::Unit::TestCase
|
5
|
+
module ObserverTests
|
6
|
+
class QueueingObserver
|
7
|
+
def initialize q
|
8
|
+
@q = q
|
9
|
+
end
|
10
|
+
|
11
|
+
def update
|
12
|
+
@q.push :foo
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_changed?
|
17
|
+
o = build_observable
|
18
|
+
refute o.changed?
|
19
|
+
o.changed
|
20
|
+
assert o.changed?
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_changed
|
24
|
+
o = build_observable
|
25
|
+
refute o.changed?
|
26
|
+
o.changed false
|
27
|
+
refute o.changed?
|
28
|
+
o.changed
|
29
|
+
assert o.changed
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_delete_observers
|
33
|
+
o = build_observable
|
34
|
+
|
35
|
+
q = Queue.new
|
36
|
+
|
37
|
+
o.add_observer QueueingObserver.new q
|
38
|
+
o.delete_observers
|
39
|
+
o.changed
|
40
|
+
o.notify_observers
|
41
|
+
assert q.empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_count_observers
|
45
|
+
o = build_observable
|
46
|
+
assert_equal 0, o.count_observers
|
47
|
+
|
48
|
+
q = Queue.new
|
49
|
+
|
50
|
+
o.add_observer QueueingObserver.new q
|
51
|
+
assert_equal 1, o.count_observers
|
52
|
+
|
53
|
+
o.add_observer QueueingObserver.new q
|
54
|
+
assert_equal 2, o.count_observers
|
55
|
+
|
56
|
+
o.delete_observers
|
57
|
+
assert_equal 0, o.count_observers
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_observer_fires
|
61
|
+
o = build_observable
|
62
|
+
q = Queue.new
|
63
|
+
|
64
|
+
o.add_observer QueueingObserver.new q
|
65
|
+
|
66
|
+
o.changed
|
67
|
+
o.notify_observers
|
68
|
+
|
69
|
+
assert_equal :foo, q.pop
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_multiple_observers
|
73
|
+
o = build_observable
|
74
|
+
q = Queue.new
|
75
|
+
|
76
|
+
o.add_observer QueueingObserver.new q
|
77
|
+
o.add_observer QueueingObserver.new q
|
78
|
+
|
79
|
+
o.changed
|
80
|
+
o.notify_observers
|
81
|
+
|
82
|
+
assert_equal :foo, q.pop
|
83
|
+
assert_equal :foo, q.pop
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_observer_only_fires_on_change
|
87
|
+
o = build_observable
|
88
|
+
q = Queue.new
|
89
|
+
|
90
|
+
o.add_observer QueueingObserver.new q
|
91
|
+
|
92
|
+
o.notify_observers
|
93
|
+
assert q.empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_delete_observer
|
97
|
+
o = build_observable
|
98
|
+
q = Queue.new
|
99
|
+
observer = QueueingObserver.new q
|
100
|
+
|
101
|
+
o.add_observer observer
|
102
|
+
|
103
|
+
o.changed
|
104
|
+
o.notify_observers
|
105
|
+
|
106
|
+
assert_equal :foo, q.pop
|
107
|
+
|
108
|
+
o.delete_observer observer
|
109
|
+
|
110
|
+
o.changed
|
111
|
+
o.notify_observers
|
112
|
+
|
113
|
+
assert q.empty?
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_delete_never_added
|
117
|
+
o = build_observable
|
118
|
+
q = Queue.new
|
119
|
+
observer = QueueingObserver.new q
|
120
|
+
|
121
|
+
o.delete_observer observer
|
122
|
+
o.changed
|
123
|
+
o.notify_observers
|
124
|
+
|
125
|
+
assert q.empty?
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_no_connection
|
129
|
+
mod = observer_module
|
130
|
+
obj = Class.new { include mod }.new
|
131
|
+
|
132
|
+
assert_raises(NameError) do
|
133
|
+
obj.changed
|
134
|
+
obj.notify_observers
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def build_observable
|
141
|
+
raise NotImplementedError
|
142
|
+
end
|
143
|
+
|
144
|
+
def observer_module
|
145
|
+
raise NotImplementedError
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'pg'
|
2
|
+
require 'tusk/observable/pg'
|
3
|
+
require 'helper'
|
4
|
+
|
5
|
+
module Tusk
|
6
|
+
module Observable
|
7
|
+
class TestPg < TestCase
|
8
|
+
include ObserverTests
|
9
|
+
|
10
|
+
class Timer
|
11
|
+
include Tusk::Observable::PG
|
12
|
+
|
13
|
+
def tick
|
14
|
+
changed
|
15
|
+
notify_observers
|
16
|
+
end
|
17
|
+
|
18
|
+
def connection
|
19
|
+
Thread.current[:conn] ||= ::PG::Connection.new :dbname => 'postgres'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def build_observable
|
26
|
+
Timer.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def observer_module
|
30
|
+
Tusk::Observable::PG
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class TestClassPg < TestCase
|
35
|
+
include ObserverTests
|
36
|
+
|
37
|
+
def build_observable
|
38
|
+
Class.new {
|
39
|
+
extend Tusk::Observable::PG
|
40
|
+
|
41
|
+
def self.tick
|
42
|
+
changed
|
43
|
+
notify_observers
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.connection
|
47
|
+
Thread.current[:conn] ||= ::PG::Connection.new :dbname => 'postgres'
|
48
|
+
end
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def observer_module
|
53
|
+
Tusk::Observable::PG
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'tusk/observable/redis'
|
3
|
+
require 'helper'
|
4
|
+
|
5
|
+
module Tusk
|
6
|
+
module Observable
|
7
|
+
class TestRedis < TestCase
|
8
|
+
include ObserverTests
|
9
|
+
|
10
|
+
class Timer
|
11
|
+
include Tusk::Observable::Redis
|
12
|
+
|
13
|
+
def tick
|
14
|
+
changed
|
15
|
+
notify_observers
|
16
|
+
end
|
17
|
+
|
18
|
+
def connection
|
19
|
+
Thread.current[:redis] ||= ::Redis.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class QueueingObserver
|
24
|
+
def initialize q
|
25
|
+
@q = q
|
26
|
+
end
|
27
|
+
|
28
|
+
def update
|
29
|
+
@q.push :foo
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def build_observable
|
36
|
+
Timer.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def observer_module
|
40
|
+
Tusk::Observable::Redis
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class TestClassRedis < TestCase
|
45
|
+
include ObserverTests
|
46
|
+
|
47
|
+
def build_observable
|
48
|
+
Class.new {
|
49
|
+
extend Tusk::Observable::Redis
|
50
|
+
|
51
|
+
def self.tick
|
52
|
+
changed
|
53
|
+
notify_observers
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.connection
|
57
|
+
Thread.current[:redis] ||= ::Redis.new
|
58
|
+
end
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def observer_module
|
63
|
+
Tusk::Observable::Redis
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
Dir.chdir(File.join(File.dirname(__FILE__), '..')) do
|
70
|
+
`redis-server redis-test.conf`
|
71
|
+
end
|
72
|
+
|
73
|
+
at_exit {
|
74
|
+
next if $!
|
75
|
+
|
76
|
+
exit_code = MiniTest::Unit.new.run(ARGV)
|
77
|
+
|
78
|
+
processes = `ps -A -o pid,command | grep [r]edis-test`.split("\n")
|
79
|
+
pids = processes.map { |process| process.split(" ")[0] }
|
80
|
+
puts "Killing test redis server..."
|
81
|
+
pids.each { |pid| Process.kill("KILL", pid.to_i) }
|
82
|
+
|
83
|
+
exit exit_code
|
84
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
daemonize yes
|
metadata
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tusk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Aaron Patterson
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-07-22 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: minitest
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 21
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 11
|
32
|
+
version: "2.11"
|
33
|
+
type: :development
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: pg
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 39
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
- 14
|
47
|
+
- 0
|
48
|
+
version: 0.14.0
|
49
|
+
type: :development
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: rdoc
|
53
|
+
prerelease: false
|
54
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ~>
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 19
|
60
|
+
segments:
|
61
|
+
- 3
|
62
|
+
- 10
|
63
|
+
version: "3.10"
|
64
|
+
type: :development
|
65
|
+
version_requirements: *id003
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: hoe
|
68
|
+
prerelease: false
|
69
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ~>
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 25
|
75
|
+
segments:
|
76
|
+
- 2
|
77
|
+
- 13
|
78
|
+
version: "2.13"
|
79
|
+
type: :development
|
80
|
+
version_requirements: *id004
|
81
|
+
description: |-
|
82
|
+
Tusk is a minimal pub / sub system with multiple observer strategies.
|
83
|
+
Tusk builds upon the Observer API from stdlib in order to provide a mostly
|
84
|
+
consistent API for building cross thread or process pub / sub systems.
|
85
|
+
|
86
|
+
Currently, Tusk supports Redis and PostgreSQL as message bus back ends.
|
87
|
+
email:
|
88
|
+
- aaron@tenderlovemaking.com
|
89
|
+
executables: []
|
90
|
+
|
91
|
+
extensions: []
|
92
|
+
|
93
|
+
extra_rdoc_files:
|
94
|
+
- Manifest.txt
|
95
|
+
- CHANGELOG.rdoc
|
96
|
+
- README.markdown
|
97
|
+
files:
|
98
|
+
- .autotest
|
99
|
+
- CHANGELOG.rdoc
|
100
|
+
- Manifest.txt
|
101
|
+
- README.markdown
|
102
|
+
- Rakefile
|
103
|
+
- lib/tusk.rb
|
104
|
+
- lib/tusk/latch.rb
|
105
|
+
- lib/tusk/observable/pg.rb
|
106
|
+
- lib/tusk/observable/redis.rb
|
107
|
+
- test/helper.rb
|
108
|
+
- test/observable/test_pg.rb
|
109
|
+
- test/observable/test_redis.rb
|
110
|
+
- test/redis-test.conf
|
111
|
+
- .gemtest
|
112
|
+
homepage: http://github.com/tenderlove/tusk
|
113
|
+
licenses: []
|
114
|
+
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options:
|
117
|
+
- --main
|
118
|
+
- README.markdown
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
hash: 3
|
127
|
+
segments:
|
128
|
+
- 0
|
129
|
+
version: "0"
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
none: false
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
hash: 3
|
136
|
+
segments:
|
137
|
+
- 0
|
138
|
+
version: "0"
|
139
|
+
requirements: []
|
140
|
+
|
141
|
+
rubyforge_project: tusk
|
142
|
+
rubygems_version: 1.8.22
|
143
|
+
signing_key:
|
144
|
+
specification_version: 3
|
145
|
+
summary: Tusk is a minimal pub / sub system with multiple observer strategies
|
146
|
+
test_files:
|
147
|
+
- test/observable/test_pg.rb
|
148
|
+
- test/observable/test_redis.rb
|