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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/.yardoc/checksums +15 -0
  6. data/.yardoc/object_types +0 -0
  7. data/.yardoc/objects/root.dat +0 -0
  8. data/.yardoc/proxy_types +0 -0
  9. data/Gemfile +23 -0
  10. data/Gemfile.lock +123 -0
  11. data/README.md +218 -0
  12. data/Rakefile +2 -0
  13. data/config.ru +7 -0
  14. data/config/tddium.yml +11 -0
  15. data/doc/Isimud.html +1696 -0
  16. data/doc/Isimud/BunnyClient.html +1004 -0
  17. data/doc/Isimud/Client.html +812 -0
  18. data/doc/Isimud/Event.html +1500 -0
  19. data/doc/Isimud/EventListener.html +1217 -0
  20. data/doc/Isimud/EventObserver.html +367 -0
  21. data/doc/Isimud/EventObserver/ClassMethods.html +292 -0
  22. data/doc/Isimud/Generators.html +117 -0
  23. data/doc/Isimud/Generators/ConfigGenerator.html +192 -0
  24. data/doc/Isimud/Generators/InitializerGenerator.html +192 -0
  25. data/doc/Isimud/Logging.html +230 -0
  26. data/doc/Isimud/ModelWatcher.html +312 -0
  27. data/doc/Isimud/ModelWatcher/ClassMethods.html +511 -0
  28. data/doc/Isimud/Railtie.html +123 -0
  29. data/doc/Isimud/TestClient.html +1003 -0
  30. data/doc/Isimud/TestClient/Queue.html +556 -0
  31. data/doc/_index.html +290 -0
  32. data/doc/class_list.html +58 -0
  33. data/doc/css/common.css +1 -0
  34. data/doc/css/full_list.css +57 -0
  35. data/doc/css/style.css +339 -0
  36. data/doc/file.README.html +338 -0
  37. data/doc/file_list.html +60 -0
  38. data/doc/frames.html +26 -0
  39. data/doc/index.html +338 -0
  40. data/doc/js/app.js +219 -0
  41. data/doc/js/full_list.js +181 -0
  42. data/doc/js/jquery.js +4 -0
  43. data/doc/method_list.html +711 -0
  44. data/doc/top-level-namespace.html +112 -0
  45. data/isimud.gemspec +25 -0
  46. data/lib/isimud.rb +91 -0
  47. data/lib/isimud/bunny_client.rb +95 -0
  48. data/lib/isimud/client.rb +48 -0
  49. data/lib/isimud/event.rb +112 -0
  50. data/lib/isimud/event_listener.rb +200 -0
  51. data/lib/isimud/event_observer.rb +81 -0
  52. data/lib/isimud/logging.rb +11 -0
  53. data/lib/isimud/model_watcher.rb +144 -0
  54. data/lib/isimud/railtie.rb +9 -0
  55. data/lib/isimud/tasks.rb +20 -0
  56. data/lib/isimud/test_client.rb +89 -0
  57. data/lib/isimud/version.rb +3 -0
  58. data/lib/rails/generators/isimud/config_generator.rb +12 -0
  59. data/lib/rails/generators/isimud/initializer_generator.rb +12 -0
  60. data/lib/rails/generators/isimud/templates/initializer.rb +17 -0
  61. data/lib/rails/generators/isimud/templates/isimud.yml +20 -0
  62. data/spec/internal/app/models/admin.rb +2 -0
  63. data/spec/internal/app/models/company.rb +34 -0
  64. data/spec/internal/app/models/user.rb +27 -0
  65. data/spec/internal/config/database.yml +3 -0
  66. data/spec/internal/config/routes.rb +3 -0
  67. data/spec/internal/db/schema.rb +22 -0
  68. data/spec/internal/log/.gitignore +1 -0
  69. data/spec/internal/public/favicon.ico +0 -0
  70. data/spec/isimud/bunny_client_spec.rb +125 -0
  71. data/spec/isimud/event_listener_spec.rb +86 -0
  72. data/spec/isimud/event_observer_spec.rb +32 -0
  73. data/spec/isimud/event_spec.rb +74 -0
  74. data/spec/isimud/model_watcher_spec.rb +189 -0
  75. data/spec/isimud/test_client_spec.rb +28 -0
  76. data/spec/isimud_spec.rb +49 -0
  77. data/spec/spec_helper.rb +55 -0
  78. metadata +195 -0
@@ -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,3 @@
1
+ module Isimud
2
+ VERSION = '0.5.2'
3
+ 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,2 @@
1
+ class Admin < User
2
+ end
@@ -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,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: db/combustion_test.sqlite
@@ -0,0 +1,3 @@
1
+ Rails.application.routes.draw do
2
+ #
3
+ 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