isimud 0.5.2
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.
- 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
|