├── .autotest ├── CHANGELOG.rdoc ├── Manifest.txt ├── README.markdown ├── Rakefile ├── lib ├── tusk.rb └── tusk │ ├── latch.rb │ └── observable │ ├── drb.rb │ ├── pg.rb │ └── redis.rb ├── test ├── helper.rb ├── observable │ ├── test_drb.rb │ ├── test_pg.rb │ └── test_redis.rb └── redis-test.conf └── tusk.gemspec /.autotest: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require 'autotest/restart' 4 | 5 | Autotest.add_hook :initialize do |at| 6 | at.testlib = 'minitest/autorun' 7 | at.find_directories = ARGV unless ARGV.empty? 8 | end 9 | -------------------------------------------------------------------------------- /CHANGELOG.rdoc: -------------------------------------------------------------------------------- 1 | === 1.1.0 / 2012-10-04 2 | 3 | * Redis supports sending a payload with `notify_observers` 4 | Thanks Collin Miller! 5 | 6 | * DRb observer added 7 | 8 | === 1.0.0 / 2012-07-18 9 | 10 | * 1 major enhancement 11 | 12 | * Birthday! 13 | 14 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 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/drb.rb 9 | lib/tusk/observable/pg.rb 10 | lib/tusk/observable/redis.rb 11 | test/helper.rb 12 | test/observable/test_drb.rb 13 | test/observable/test_pg.rb 14 | test/observable/test_redis.rb 15 | test/redis-test.conf 16 | tusk.gemspec 17 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 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 | * Supports DRb as a message bus 19 | 20 | ## SYNOPSIS: 21 | 22 | Here is an in-memory observer example: 23 | 24 | ```ruby 25 | require 'observer' 26 | 27 | class Timer 28 | include Observable 29 | 30 | def tick 31 | changed 32 | notify_observers 33 | end 34 | end 35 | 36 | class Listener 37 | def update; puts "got update"; end 38 | end 39 | 40 | timer = Timer.new 41 | timer.add_observer Listener.new 42 | loop { timer.tick; sleep 1; } 43 | ``` 44 | 45 | 46 | The down side of this example is that the Listener cannot be in a different 47 | process. We can move the Listener to a different process by using the Redis 48 | observable mixin and providing a redis connection: 49 | 50 | ```ruby 51 | require 'tusk/observable/redis' 52 | require 'redis' 53 | 54 | class Timer 55 | include Tusk::Observable::Redis 56 | 57 | def tick 58 | changed 59 | notify_observers 60 | end 61 | 62 | def connection 63 | Thread.current[:redis] ||= ::Redis.new 64 | end 65 | end 66 | 67 | class Listener 68 | def update; puts "got update PID: #{$$}"; end 69 | end 70 | 71 | timer = Timer.new 72 | 73 | fork { 74 | timer.add_observer Listener.new 75 | sleep 76 | } 77 | 78 | loop { timer.tick; sleep 1; } 79 | ``` 80 | 81 | PostgreSQL can also be used as the message bus: 82 | 83 | ```ruby 84 | require 'tusk/observable/pg' 85 | require 'pg' 86 | 87 | class Timer 88 | include Tusk::Observable::PG 89 | 90 | def tick 91 | changed 92 | notify_observers 93 | end 94 | 95 | def connection 96 | Thread.current[:pg] ||= ::PG::Connection.new :dbname => 'postgres' 97 | end 98 | end 99 | 100 | class Listener 101 | def update; puts "got update PID: #{$$}"; end 102 | end 103 | 104 | timer = Timer.new 105 | 106 | fork { 107 | timer.add_observer Listener.new 108 | sleep 109 | } 110 | 111 | loop { timer.tick; sleep 1; } 112 | ``` 113 | 114 | We can easily integrate Tusk with Active Record. Here is a User model that 115 | sends notifications when a user is created: 116 | 117 | ```ruby 118 | require 'tusk/observable/pg' 119 | class User < ActiveRecord::Base 120 | attr_accessible :name 121 | 122 | extend Tusk::Observable::PG 123 | 124 | # After users are created, notify the message bus 125 | after_create :notify_observers 126 | 127 | # Listeners will use the table name as the bus channel 128 | def self.channel 129 | table_name 130 | end 131 | 132 | private 133 | 134 | def notify_observers 135 | self.class.changed 136 | self.class.notify_observers 137 | end 138 | end 139 | ``` 140 | 141 | The table name is used as the channel name where objects will listen. Here is 142 | a producer script: 143 | 144 | ```ruby 145 | require 'user' 146 | loop do 147 | User.create!(:name => 'testing') 148 | sleep 1 149 | end 150 | ``` 151 | 152 | Our consumer adds an observer to the User class: 153 | 154 | ```ruby 155 | require 'user' 156 | class UserListener 157 | def initialize 158 | super 159 | @last_id = 0 160 | end 161 | 162 | def update 163 | users = User.where('id > ?', @last_id).sort_by(&:id) 164 | @last_id = users.last.id 165 | users.each { |u| p "user created: #{u.id}" } 166 | end 167 | end 168 | 169 | User.add_observer UserListener.new 170 | # Put the main thread to sleep 171 | sleep 172 | ``` 173 | 174 | Whenever a user gets created, our consumer listener will be notified. 175 | 176 | ## REQUIREMENTS: 177 | 178 | * PostgreSQL or Redis 179 | 180 | ## INSTALL: 181 | 182 | * gem install tusk 183 | 184 | ## LICENSE: 185 | 186 | (The MIT License) 187 | 188 | Copyright (c) 2012 Aaron Patterson 189 | 190 | Permission is hereby granted, free of charge, to any person obtaining 191 | a copy of this software and associated documentation files (the 192 | 'Software'), to deal in the Software without restriction, including 193 | without limitation the rights to use, copy, modify, merge, publish, 194 | distribute, sublicense, and/or sell copies of the Software, and to 195 | permit persons to whom the Software is furnished to do so, subject to 196 | the following conditions: 197 | 198 | The above copyright notice and this permission notice shall be 199 | included in all copies or substantial portions of the Software. 200 | 201 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 202 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 203 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 204 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 205 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 206 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 207 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 208 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 21 | -------------------------------------------------------------------------------- /lib/tusk.rb: -------------------------------------------------------------------------------- 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. Tusk::Observers::DRb offers an 7 | # Observer API with DRb as the message bus. 8 | module Tusk 9 | VERSION = '1.1.0' 10 | end 11 | -------------------------------------------------------------------------------- /lib/tusk/latch.rb: -------------------------------------------------------------------------------- 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 25 | -------------------------------------------------------------------------------- /lib/tusk/observable/drb.rb: -------------------------------------------------------------------------------- 1 | require 'drb' 2 | require 'digest/md5' 3 | 4 | module Tusk 5 | module Observable 6 | ### 7 | # An observer implementation for DRb. This module requires that 8 | # you start a DRb server, which can be done via Server.start 9 | # 10 | # This observer works across processes. 11 | # 12 | # Example: 13 | # 14 | # require 'tusk/observable/drb' 15 | # 16 | # class Timer 17 | # include Tusk::Observable::DRb 18 | # 19 | # # Start the DRb server. Do this once 20 | # Thread.new { Server.start } 21 | # 22 | # def tick 23 | # changed 24 | # notify_observers 25 | # end 26 | # end 27 | # 28 | # class Listener 29 | # def update 30 | # puts "got update" 31 | # end 32 | # end 33 | # 34 | # timer = Timer.new 35 | # 36 | # fork do 37 | # timer.add_observer Listener.new 38 | # sleep # put the process to sleep so it doesn't exit 39 | # end 40 | # 41 | # loop do 42 | # timer.tick 43 | # sleep 1 44 | # end 45 | module DRb 46 | class Server 47 | URI = 'druby://localhost:8787' 48 | 49 | def self.start 50 | ::DRb.start_service URI, new 51 | end 52 | 53 | def self.stop 54 | ::DRb.stop_service 55 | end 56 | 57 | def initialize 58 | @channels = Hash.new { |h,k| h[k] = {} } 59 | end 60 | 61 | def watch channel, proxy 62 | @channels[channel][proxy] = proxy 63 | end 64 | 65 | def signal channel, args 66 | @channels[channel].each { |proxy,| 67 | proxy.notify args 68 | } 69 | end 70 | 71 | def delete_observer channel, o 72 | @channels[channel].delete o 73 | end 74 | 75 | def delete channel 76 | @channels.delete channel 77 | end 78 | end 79 | 80 | class Proxy # :nodoc: 81 | include ::DRb::DRbUndumped 82 | 83 | def initialize d, func 84 | @delegate = d 85 | @func = func 86 | end 87 | 88 | def notify args 89 | @delegate.send(@func, *args) 90 | end 91 | end 92 | 93 | def self.extended klass 94 | super 95 | 96 | klass.instance_eval do 97 | @bus = DRbObject.new_with_uri uri 98 | @observer_state = false 99 | @subscribers = {} 100 | end 101 | end 102 | 103 | def initialize *args 104 | super 105 | 106 | @bus = DRbObject.new_with_uri uri 107 | @observer_state = false 108 | @subscribers = {} 109 | end 110 | 111 | # Add +observer+ as an observer to this object. The +object+ will 112 | # receive a notification when #changed? returns true and #notify_observers 113 | # is called. 114 | # 115 | # +func+ method is called on +object+ when notifications are sent. 116 | def add_observer object, func = :update 117 | unless ::DRb.thread && ::DRb.thread.alive? 118 | ::DRb.start_service 119 | end 120 | 121 | proxy = Proxy.new object, func 122 | @subscribers[object] = proxy 123 | @bus.watch channel, proxy 124 | end 125 | 126 | # If this object's #changed? state is true, this method will notify 127 | # observing objects. 128 | def notify_observers(*args) 129 | return unless changed? 130 | @bus.signal channel, args 131 | changed false 132 | end 133 | 134 | # Remove all observers associated with this object *in the current 135 | # process*. This method will not impact observers of this object in 136 | # other processes. 137 | def delete_observers 138 | @bus.delete channel 139 | @subscribers.clear 140 | end 141 | 142 | # Remove +observer+ so that it will no longer receive notifications. 143 | def delete_observer o 144 | proxy = @subscribers.delete o 145 | @bus.delete_observer channel, proxy 146 | end 147 | 148 | # Returns true if this object's state has been changed since the last 149 | # call to #notify_observers. 150 | def changed? 151 | @observer_state 152 | end 153 | 154 | # Set the changed state of this object. Notifications will be sent only 155 | # if the changed +state+ is a truthy object. 156 | def changed state = true 157 | @observer_state = state 158 | end 159 | 160 | # Returns the number of observers associated with this object *in the 161 | # current process*. If the object is observed across multiple processes, 162 | # the returned count will not reflect the other processes. 163 | def count_observers 164 | @subscribers.length 165 | end 166 | 167 | private 168 | 169 | def uri 170 | Server::URI 171 | end 172 | 173 | def channel 174 | "a" + Digest::MD5.hexdigest("#{self.class.name}#{object_id}") 175 | end 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/tusk/observable/pg.rb: -------------------------------------------------------------------------------- 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 171 | -------------------------------------------------------------------------------- /lib/tusk/observable/redis.rb: -------------------------------------------------------------------------------- 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(*args) 104 | return unless changed? 105 | connection.publish channel, payload_coder.dump(args) 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 payload_coder 152 | Marshal 153 | end 154 | 155 | def start_listener latch 156 | connection.subscribe(channel, control_channel) do |on| 157 | on.subscribe { |c| latch.release } 158 | 159 | on.message do |c, message| 160 | if c == control_channel && message == 'quit' 161 | connection.unsubscribe 162 | else 163 | @sub_lock.synchronize do 164 | subscribers.fetch(c, {}).each do |object,m| 165 | object.send m, *payload_coder.load(message) 166 | end 167 | end 168 | end 169 | end 170 | end 171 | end 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 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 | class PayloadQueueingObserver < QueueingObserver 17 | def update(*args) 18 | @q.push(args) 19 | end 20 | end 21 | 22 | def test_changed? 23 | o = build_observable 24 | refute o.changed? 25 | o.changed 26 | assert o.changed? 27 | end 28 | 29 | def test_changed 30 | o = build_observable 31 | refute o.changed? 32 | o.changed false 33 | refute o.changed? 34 | o.changed 35 | assert o.changed 36 | end 37 | 38 | def test_delete_observers 39 | o = build_observable 40 | 41 | q = Queue.new 42 | 43 | o.add_observer QueueingObserver.new q 44 | o.delete_observers 45 | o.changed 46 | o.notify_observers 47 | assert q.empty? 48 | end 49 | 50 | def test_count_observers 51 | o = build_observable 52 | assert_equal 0, o.count_observers 53 | 54 | q = Queue.new 55 | 56 | o.add_observer QueueingObserver.new q 57 | assert_equal 1, o.count_observers 58 | 59 | o.add_observer QueueingObserver.new q 60 | assert_equal 2, o.count_observers 61 | 62 | o.delete_observers 63 | assert_equal 0, o.count_observers 64 | end 65 | 66 | def test_observer_fires 67 | o = build_observable 68 | q = Queue.new 69 | 70 | o.add_observer QueueingObserver.new q 71 | 72 | o.changed 73 | o.notify_observers 74 | 75 | assert_equal :foo, q.pop 76 | end 77 | 78 | def test_notification_payload 79 | o = build_observable 80 | q = Queue.new 81 | 82 | o.add_observer PayloadQueueingObserver.new q 83 | 84 | o.changed 85 | o.notify_observers :payload 86 | 87 | assert_equal [:payload], q.pop 88 | end 89 | 90 | def test_multiple_observers 91 | o = build_observable 92 | q = Queue.new 93 | 94 | o.add_observer QueueingObserver.new q 95 | o.add_observer QueueingObserver.new q 96 | 97 | o.changed 98 | o.notify_observers 99 | 100 | assert_equal :foo, q.pop 101 | assert_equal :foo, q.pop 102 | end 103 | 104 | def test_observer_only_fires_on_change 105 | o = build_observable 106 | q = Queue.new 107 | 108 | o.add_observer QueueingObserver.new q 109 | 110 | o.notify_observers 111 | assert q.empty? 112 | end 113 | 114 | def test_delete_observer 115 | o = build_observable 116 | q = Queue.new 117 | observer = QueueingObserver.new q 118 | 119 | o.add_observer observer 120 | 121 | o.changed 122 | o.notify_observers 123 | 124 | assert_equal :foo, q.pop 125 | 126 | o.delete_observer observer 127 | 128 | o.changed 129 | o.notify_observers 130 | 131 | assert q.empty? 132 | end 133 | 134 | def test_delete_never_added 135 | o = build_observable 136 | q = Queue.new 137 | observer = QueueingObserver.new q 138 | 139 | o.delete_observer observer 140 | o.changed 141 | o.notify_observers 142 | 143 | assert q.empty? 144 | end 145 | 146 | def test_no_connection 147 | mod = observer_module 148 | obj = Class.new { include mod }.new 149 | 150 | assert_raises(NameError) do 151 | obj.changed 152 | obj.notify_observers 153 | end 154 | end 155 | 156 | private 157 | 158 | def build_observable 159 | raise NotImplementedError 160 | end 161 | 162 | def observer_module 163 | raise NotImplementedError 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /test/observable/test_drb.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | require 'tusk/observable/drb' 3 | 4 | module Tusk 5 | module Observable 6 | class TestDRb < TestCase 7 | include ObserverTests 8 | 9 | class Timer 10 | include Tusk::Observable::DRb 11 | 12 | def tick 13 | changed 14 | notify_observers 15 | end 16 | end 17 | 18 | def setup 19 | super 20 | DRb::Server.start 21 | end 22 | 23 | def teardown 24 | super 25 | DRb::Server.stop 26 | end 27 | 28 | def test_no_connection 29 | skip "not implementing for now" 30 | end 31 | 32 | private 33 | 34 | def build_observable 35 | Timer.new 36 | end 37 | 38 | def observer_module 39 | Tusk::Observable::DRb 40 | end 41 | end 42 | 43 | class TestClassDRb < TestCase 44 | include ObserverTests 45 | 46 | def setup 47 | super 48 | DRb::Server.start 49 | end 50 | 51 | def teardown 52 | super 53 | DRb::Server.stop 54 | end 55 | 56 | def build_observable 57 | Class.new { 58 | extend Tusk::Observable::DRb 59 | 60 | def self.tick 61 | changed 62 | notify_observers 63 | end 64 | } 65 | end 66 | 67 | def test_no_connection 68 | skip "not implementing for now" 69 | end 70 | 71 | def observer_module 72 | Tusk::Observable::DRb 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/observable/test_pg.rb: -------------------------------------------------------------------------------- 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 58 | -------------------------------------------------------------------------------- /test/observable/test_redis.rb: -------------------------------------------------------------------------------- 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 | } 85 | -------------------------------------------------------------------------------- /test/redis-test.conf: -------------------------------------------------------------------------------- 1 | daemonize yes 2 | -------------------------------------------------------------------------------- /tusk.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "tusk" 5 | s.version = "1.1.0.20121229121018" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Aaron Patterson"] 9 | s.date = "2012-12-29" 10 | s.description = "Tusk is a minimal pub / sub system with multiple observer strategies.\nTusk builds upon the Observer API from stdlib in order to provide a mostly\nconsistent API for building cross thread or process pub / sub systems.\n\nCurrently, Tusk supports Redis and PostgreSQL as message bus back ends." 11 | s.email = ["aaron@tenderlovemaking.com"] 12 | s.extra_rdoc_files = ["CHANGELOG.rdoc", "Manifest.txt", "CHANGELOG.rdoc", "README.markdown"] 13 | s.files = [".autotest", "CHANGELOG.rdoc", "Manifest.txt", "README.markdown", "Rakefile", "lib/tusk.rb", "lib/tusk/latch.rb", "lib/tusk/observable/drb.rb", "lib/tusk/observable/pg.rb", "lib/tusk/observable/redis.rb", "test/helper.rb", "test/observable/test_drb.rb", "test/observable/test_pg.rb", "test/observable/test_redis.rb", "test/redis-test.conf", "tusk.gemspec", ".gemtest"] 14 | s.homepage = "http://github.com/tenderlove/tusk" 15 | s.rdoc_options = ["--main", "README.markdown"] 16 | s.require_paths = ["lib"] 17 | s.rubyforge_project = "tusk" 18 | s.rubygems_version = "2.0.0.preview3" 19 | s.summary = "Tusk is a minimal pub / sub system with multiple observer strategies" 20 | s.test_files = ["test/observable/test_drb.rb", "test/observable/test_pg.rb", "test/observable/test_redis.rb"] 21 | 22 | if s.respond_to? :specification_version then 23 | s.specification_version = 4 24 | 25 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 26 | s.add_development_dependency(%q, ["~> 4.3"]) 27 | s.add_development_dependency(%q, ["~> 3.10"]) 28 | s.add_development_dependency(%q, ["~> 0.14.0"]) 29 | s.add_development_dependency(%q, ["~> 3.3"]) 30 | else 31 | s.add_dependency(%q, ["~> 4.3"]) 32 | s.add_dependency(%q, ["~> 3.10"]) 33 | s.add_dependency(%q, ["~> 0.14.0"]) 34 | s.add_dependency(%q, ["~> 3.3"]) 35 | end 36 | else 37 | s.add_dependency(%q, ["~> 4.3"]) 38 | s.add_dependency(%q, ["~> 3.10"]) 39 | s.add_dependency(%q, ["~> 0.14.0"]) 40 | s.add_dependency(%q, ["~> 3.3"]) 41 | end 42 | end 43 | --------------------------------------------------------------------------------