isimud 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.yardoc/checksums +15 -0
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/Gemfile +23 -0
- data/Gemfile.lock +123 -0
- data/README.md +218 -0
- data/Rakefile +2 -0
- data/config.ru +7 -0
- data/config/tddium.yml +11 -0
- data/doc/Isimud.html +1696 -0
- data/doc/Isimud/BunnyClient.html +1004 -0
- data/doc/Isimud/Client.html +812 -0
- data/doc/Isimud/Event.html +1500 -0
- data/doc/Isimud/EventListener.html +1217 -0
- data/doc/Isimud/EventObserver.html +367 -0
- data/doc/Isimud/EventObserver/ClassMethods.html +292 -0
- data/doc/Isimud/Generators.html +117 -0
- data/doc/Isimud/Generators/ConfigGenerator.html +192 -0
- data/doc/Isimud/Generators/InitializerGenerator.html +192 -0
- data/doc/Isimud/Logging.html +230 -0
- data/doc/Isimud/ModelWatcher.html +312 -0
- data/doc/Isimud/ModelWatcher/ClassMethods.html +511 -0
- data/doc/Isimud/Railtie.html +123 -0
- data/doc/Isimud/TestClient.html +1003 -0
- data/doc/Isimud/TestClient/Queue.html +556 -0
- data/doc/_index.html +290 -0
- data/doc/class_list.html +58 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +57 -0
- data/doc/css/style.css +339 -0
- data/doc/file.README.html +338 -0
- data/doc/file_list.html +60 -0
- data/doc/frames.html +26 -0
- data/doc/index.html +338 -0
- data/doc/js/app.js +219 -0
- data/doc/js/full_list.js +181 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +711 -0
- data/doc/top-level-namespace.html +112 -0
- data/isimud.gemspec +25 -0
- data/lib/isimud.rb +91 -0
- data/lib/isimud/bunny_client.rb +95 -0
- data/lib/isimud/client.rb +48 -0
- data/lib/isimud/event.rb +112 -0
- data/lib/isimud/event_listener.rb +200 -0
- data/lib/isimud/event_observer.rb +81 -0
- data/lib/isimud/logging.rb +11 -0
- data/lib/isimud/model_watcher.rb +144 -0
- data/lib/isimud/railtie.rb +9 -0
- data/lib/isimud/tasks.rb +20 -0
- data/lib/isimud/test_client.rb +89 -0
- data/lib/isimud/version.rb +3 -0
- data/lib/rails/generators/isimud/config_generator.rb +12 -0
- data/lib/rails/generators/isimud/initializer_generator.rb +12 -0
- data/lib/rails/generators/isimud/templates/initializer.rb +17 -0
- data/lib/rails/generators/isimud/templates/isimud.yml +20 -0
- data/spec/internal/app/models/admin.rb +2 -0
- data/spec/internal/app/models/company.rb +34 -0
- data/spec/internal/app/models/user.rb +27 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/schema.rb +22 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/isimud/bunny_client_spec.rb +125 -0
- data/spec/isimud/event_listener_spec.rb +86 -0
- data/spec/isimud/event_observer_spec.rb +32 -0
- data/spec/isimud/event_spec.rb +74 -0
- data/spec/isimud/model_watcher_spec.rb +189 -0
- data/spec/isimud/test_client_spec.rb +28 -0
- data/spec/isimud_spec.rb +49 -0
- data/spec/spec_helper.rb +55 -0
- metadata +195 -0
data/lib/isimud/tasks.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
namespace :isimud do
|
2
|
+
desc 'Synchronize specified models (default is all synchronized models)'
|
3
|
+
task :sync => :environment do
|
4
|
+
require 'chronic_duration'
|
5
|
+
require 'isimud'
|
6
|
+
|
7
|
+
models = $*.drop(1)
|
8
|
+
models = Isimud::ModelWatcher.watched_models if models.empty?
|
9
|
+
|
10
|
+
start_time = Time.now
|
11
|
+
puts "Synchronizing models: #{models.join(', ')}"
|
12
|
+
models.each do |model|
|
13
|
+
klass = model.constantize
|
14
|
+
puts "\n#{klass.to_s}"
|
15
|
+
klass.synchronize(output: $stdout)
|
16
|
+
end
|
17
|
+
end_time = Time.now
|
18
|
+
puts "Finished synchronization in #{ChronicDuration.output(end_time - start_time)}."
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Isimud
|
2
|
+
class TestClient < Isimud::Client
|
3
|
+
attr_accessor :queues
|
4
|
+
|
5
|
+
class Queue
|
6
|
+
include Isimud::Logging
|
7
|
+
attr_reader :name, :routing_keys
|
8
|
+
|
9
|
+
def initialize(name, listener)
|
10
|
+
@name = name
|
11
|
+
@listener = listener
|
12
|
+
@routing_keys = Set.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def bind(exchange, options = {})
|
16
|
+
key = "\\A#{options[:routing_key]}\\Z"
|
17
|
+
log "TestClient: adding routing key #{key} to queue #{name}"
|
18
|
+
@routing_keys << Regexp.new(key.gsub(/\./, "\\.").gsub(/\*/, ".*"))
|
19
|
+
end
|
20
|
+
|
21
|
+
def has_matching_key?(route)
|
22
|
+
@routing_keys.any? { |k| route =~ k }
|
23
|
+
end
|
24
|
+
|
25
|
+
def deliver(data)
|
26
|
+
begin
|
27
|
+
@listener.call(data)
|
28
|
+
rescue => e
|
29
|
+
log "TestClient: error delivering message: #{e.message}\n #{e.backtrace.join("\n ")}", :error
|
30
|
+
@listener.exception_handler.try(:call, e)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(connection = nil, options = nil)
|
36
|
+
self.queues = Hash.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def connect
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def channel
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def connected?
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
def close
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete_queue(queue_name)
|
55
|
+
queues.delete(queue_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def bind(queue_name, exchange_name, *keys, &method)
|
59
|
+
create_queue(queue_name, exchange_name, routing_keys: keys, &method)
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_queue(queue_name, exchange_name, options = {}, &method)
|
63
|
+
keys = options[:routing_keys] || []
|
64
|
+
log "Isimud::TestClient: Binding queue #{queue_name} for keys #{keys.inspect}"
|
65
|
+
queue = queues[queue_name] ||= Queue.new(queue_name, method)
|
66
|
+
keys.each do |k|
|
67
|
+
queue.bind(exchange_name, routing_key: k)
|
68
|
+
end
|
69
|
+
queue
|
70
|
+
end
|
71
|
+
|
72
|
+
def publish(exchange, routing_key, payload)
|
73
|
+
log "Isimud::TestClient: Delivering message key: #{routing_key} payload: #{payload}"
|
74
|
+
call_queues = queues.values.select { |queue| queue.has_matching_key?(routing_key) }
|
75
|
+
call_queues.each do |queue|
|
76
|
+
log "Isimud::TestClient: Queue #{queue.name} matches routing key #{routing_key}"
|
77
|
+
queue.deliver(payload)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def reset
|
82
|
+
self.queues.clear
|
83
|
+
end
|
84
|
+
|
85
|
+
def reconnect
|
86
|
+
self
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Isimud
|
2
|
+
module Generators
|
3
|
+
class ConfigGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
|
5
|
+
|
6
|
+
desc 'Creates an Isimud gem configuration file at config/isimud.yml'
|
7
|
+
def create_config_file
|
8
|
+
template 'isimud.yml', File.join(Rails.root, 'config', 'isimud.yml')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Isimud
|
2
|
+
module Generators
|
3
|
+
class InitializerGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
|
5
|
+
|
6
|
+
desc 'Creates an Isimud gem initializer file at config/isimud.yml'
|
7
|
+
def create_initializer_file
|
8
|
+
template 'initializer.rb', File.join(Rails.root, 'config', 'initializers', 'isimud.rb')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'isimud'
|
2
|
+
|
3
|
+
configs = begin
|
4
|
+
path = Rails.root.join('config', 'isimud.yml')
|
5
|
+
YAML::load(ERB.new(IO.read(path)).result)
|
6
|
+
rescue
|
7
|
+
Rails.logger.warn("Isimud: configuration could not be loaded at: #{path}")
|
8
|
+
{}
|
9
|
+
end
|
10
|
+
|
11
|
+
Isimud.model_watcher_schema = Rails.configuration.database_configuration[Rails.env]['database']
|
12
|
+
|
13
|
+
config = configs[Rails.env]
|
14
|
+
|
15
|
+
config.each do |key, val|
|
16
|
+
Isimud.send("#{key}=".to_sym, val)
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Isimud configuration
|
2
|
+
# Server may be specified as a URL string, or a hash of connection options accepted by Bunny::Session
|
3
|
+
defaults: &defaults
|
4
|
+
client_type: :bunny
|
5
|
+
server: 'amqp://guest:guest@localhost'
|
6
|
+
|
7
|
+
development:
|
8
|
+
server: 'amqp://guest:guest@localhost'
|
9
|
+
|
10
|
+
test:
|
11
|
+
client_type: :test
|
12
|
+
|
13
|
+
develop:
|
14
|
+
<<: *defaults
|
15
|
+
|
16
|
+
staging:
|
17
|
+
<<: *defaults
|
18
|
+
|
19
|
+
production:
|
20
|
+
<<: *defaults
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative "../../../../lib/isimud"
|
2
|
+
|
3
|
+
class Company < ActiveRecord::Base
|
4
|
+
include Isimud::EventObserver
|
5
|
+
|
6
|
+
has_many :users
|
7
|
+
|
8
|
+
def self.find_active_observers
|
9
|
+
where(active: true).all
|
10
|
+
end
|
11
|
+
|
12
|
+
def enable_listener?
|
13
|
+
active
|
14
|
+
end
|
15
|
+
|
16
|
+
def routing_keys
|
17
|
+
["*.User.create", "*.User.destroy"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def handle_event(event)
|
21
|
+
user = User.find(event.parameters[:id])
|
22
|
+
return unless user.company_id == id
|
23
|
+
case event.action.to_s
|
24
|
+
when 'create'
|
25
|
+
reload
|
26
|
+
update_attributes!(user_count: user_count + 1)
|
27
|
+
when 'destroy'
|
28
|
+
reload
|
29
|
+
update_attributes!(user_count: user_count - 1)
|
30
|
+
else
|
31
|
+
raise "unexpected action: #{event.action}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative "../../../../lib/isimud"
|
2
|
+
|
3
|
+
class User < ActiveRecord::Base
|
4
|
+
include Isimud::EventObserver
|
5
|
+
|
6
|
+
belongs_to :company
|
7
|
+
|
8
|
+
attr_accessor :events, :routing_keys
|
9
|
+
|
10
|
+
scope :active, -> {where('deactivated != ?', true)}
|
11
|
+
|
12
|
+
def handle_event(event)
|
13
|
+
self.events ||= Array.new
|
14
|
+
self.events << event
|
15
|
+
end
|
16
|
+
|
17
|
+
def queue_prefix
|
18
|
+
'test'
|
19
|
+
end
|
20
|
+
|
21
|
+
watch_attributes :key, :login_count
|
22
|
+
|
23
|
+
def key
|
24
|
+
Base64.encode64("user-#{id}")
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
create_table(:companies, :force => true) do |t|
|
3
|
+
t.string :name
|
4
|
+
t.string :description
|
5
|
+
t.string :url
|
6
|
+
t.integer :user_count, default: 0, null: false
|
7
|
+
t.boolean :active, default: true, null: false
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
create_table(:users, :force => true) do |t|
|
12
|
+
t.references :company
|
13
|
+
t.string :first_name
|
14
|
+
t.string :last_name
|
15
|
+
t.string :encrypted_password
|
16
|
+
t.string :email
|
17
|
+
t.boolean :is_admin
|
18
|
+
t.boolean :deactivated
|
19
|
+
t.integer :login_count, default: 0, null: false
|
20
|
+
t.timestamps
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
*.log
|
File without changes
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Isimud::BunnyClient do
|
4
|
+
before(:each) do
|
5
|
+
@exchange_name = 'isimud_test'
|
6
|
+
@url = 'amqp://guest:guest@localhost'
|
7
|
+
end
|
8
|
+
|
9
|
+
let!(:client) { Isimud::BunnyClient.new(@url) }
|
10
|
+
let!(:connection) { client.connection }
|
11
|
+
|
12
|
+
describe '#initialize' do
|
13
|
+
it 'sets the broker URL' do
|
14
|
+
expect(client.url).to eq(@url)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#bind' do
|
19
|
+
let(:channel) { client.channel }
|
20
|
+
let(:proc) { Proc.new { puts('hello') } }
|
21
|
+
let(:keys) { %w(foo.bar baz.*) }
|
22
|
+
before do
|
23
|
+
Isimud.logger = Logger.new(STDOUT)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'creates a new queue' do
|
27
|
+
queue = client.bind('my_queue', @exchange_name, keys, &proc)
|
28
|
+
expect(queue).to be_a Bunny::Queue
|
29
|
+
expect(queue.name).to eq('my_queue')
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'binds specified routing keys and subscribes to the specified exchange' do
|
33
|
+
queue = double('queue', bind: 'ok')
|
34
|
+
expect(channel).to receive(:queue).and_return(queue)
|
35
|
+
expect(queue).to receive(:subscribe).with(manual_ack: true)
|
36
|
+
keys.each { |key| expect(queue).to receive(:bind).with(@exchange_name, routing_key: key, nowait: false).once }
|
37
|
+
client.bind('my_queue', @exchange_name, *keys, proc)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'calls block when a message is received' do
|
41
|
+
@block_called = Array.new
|
42
|
+
queue_name = 'test queue'
|
43
|
+
client.bind("#{queue_name}", @exchange_name, 'my.test.key') do |payload|
|
44
|
+
@block_called << payload
|
45
|
+
end
|
46
|
+
client.publish(@exchange_name, 'my.test.key', "Hi there")
|
47
|
+
expect(@block_called).to eq ['Hi there']
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#connection' do
|
52
|
+
it 'returns a Bunny session' do
|
53
|
+
expect(connection).to be_a Bunny::Session
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'sets and reuses the connection' do
|
57
|
+
connection = client.connection
|
58
|
+
expect(client.connection).to eql(connection)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'opens a connection to the broker' do
|
62
|
+
expect(connection).to be_open
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#channel' do
|
68
|
+
it 'returns a channel' do
|
69
|
+
expect(client.channel).to be_a Bunny::Channel
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'reuses an open channel' do
|
73
|
+
expect(client.channel).to eql(client.channel)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'creates a new channel if the previous one is closed' do
|
77
|
+
closed_channel = client.channel.tap(&:close)
|
78
|
+
expect(client.channel).not_to eql(closed_channel)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'enables confirmations' do
|
82
|
+
channel = client.channel
|
83
|
+
expect(channel.next_publish_seq_no).to eq(1)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'keeps the channel thread local' do
|
87
|
+
channel = client.channel
|
88
|
+
t = Thread.new do
|
89
|
+
expect(client.channel).not_to eql(channel)
|
90
|
+
end
|
91
|
+
t.join
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe '#connected?' do
|
96
|
+
it 'is true for an open session' do
|
97
|
+
expect(client).to be_connected
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'is false for a closed session' do
|
101
|
+
client.close
|
102
|
+
expect(client).not_to be_connected
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe '#close' do
|
107
|
+
it 'closes the session' do
|
108
|
+
connection = client.connection
|
109
|
+
client.close
|
110
|
+
expect(connection).not_to be_open
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe '#publish' do
|
115
|
+
let(:channel) { client.channel }
|
116
|
+
it 'sends the data with the appropriate routing key to the exchange' do
|
117
|
+
payload = {a: '123', b: 'this is b'}
|
118
|
+
topic = double(:topic)
|
119
|
+
expect(channel).to receive(:topic).with(@exchange_name, durable: true).and_return(topic)
|
120
|
+
expect(topic).to receive(:publish).with(payload, routing_key: 'foo.bar.baz', persistent: true)
|
121
|
+
client.publish(@exchange_name, 'foo.bar.baz', payload)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Isimud::EventListener do
|
4
|
+
let!(:company) { Company.create(name: 'Google', active: true) }
|
5
|
+
let!(:inactive_company) { Company.create(name: 'Radio Shack', active: false) }
|
6
|
+
let!(:listener) { Isimud::EventListener.new(name: 'all_ears', error_limit: 5, exchange: 'test-listener') }
|
7
|
+
|
8
|
+
after(:each) do
|
9
|
+
Isimud.client.reset
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'initialization' do
|
13
|
+
it 'sets parameters' do
|
14
|
+
listener = Isimud::EventListener.new(events_exchange: 'parties',
|
15
|
+
models_exchange: 'hot_bods',
|
16
|
+
error_limit: 1,
|
17
|
+
error_interval: 5.minutes,
|
18
|
+
name: 'ear')
|
19
|
+
expect(listener.events_exchange).to eq('parties')
|
20
|
+
expect(listener.models_exchange).to eq('hot_bods')
|
21
|
+
expect(listener.error_limit).to eq(1)
|
22
|
+
expect(listener.error_interval).to eq(5.minutes)
|
23
|
+
expect(listener.name).to eq('ear')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'applies defaults' do
|
27
|
+
listener = Isimud::EventListener.new
|
28
|
+
expect(listener.events_exchange).to eq('events')
|
29
|
+
expect(listener.models_exchange).to eq('models')
|
30
|
+
expect(listener.error_limit).to eq(10)
|
31
|
+
expect(listener.error_interval).to eq(1.hour)
|
32
|
+
expect(listener.name).to eq('combustion-listener')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'message delivery' do
|
37
|
+
before do
|
38
|
+
listener.bind_queues
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#bind_queues' do
|
42
|
+
it 'registers active observers' do
|
43
|
+
expect(listener).to have_observer(company)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'initializes an observer_queue' do
|
47
|
+
expect(listener.instance_variable_get(:@observer_queue)).to be_present
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'skips inactive observers' do
|
51
|
+
expect(listener).not_to have_observer(inactive_company)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe 'handling messages' do
|
56
|
+
it 'dispatches events to observer' do
|
57
|
+
expect {
|
58
|
+
User.create!(company: company, first_name: 'Larry', last_name: 'Page')
|
59
|
+
company.reload
|
60
|
+
}.to change(company, :user_count)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'handling observer updates' do
|
65
|
+
it 'registers a new observer' do
|
66
|
+
another_company = Company.create!(name: 'Apple', active: true)
|
67
|
+
expect(listener.has_observer?(another_company)).to eql(true)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 're-registers an updated observer' do
|
71
|
+
expect(listener.has_observer?(company)).to eql(true)
|
72
|
+
company.update_attributes!(active: false)
|
73
|
+
expect(listener.has_observer?(company)).to eql(false)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'does not register an observer when listening is disabled'
|
77
|
+
|
78
|
+
it 'purges the queue for a deleted observer'
|
79
|
+
end
|
80
|
+
|
81
|
+
describe 'handling errors' do
|
82
|
+
it 'counts errors'
|
83
|
+
it 'triggers a shutdown if errors exceed limit'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|