isimud 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
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,112 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
+ <head>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
6
+ <title>
7
+ Top Level Namespace
8
+
9
+ &mdash; Documentation by YARD 0.8.7.6
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" charset="utf-8" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" charset="utf-8" />
16
+
17
+ <script type="text/javascript" charset="utf-8">
18
+ hasFrames = window.top.frames.main ? true : false;
19
+ relpath = '';
20
+ framesUrl = "frames.html#!top-level-namespace.html";
21
+ </script>
22
+
23
+
24
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
25
+
26
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
27
+
28
+
29
+ </head>
30
+ <body>
31
+ <div id="header">
32
+ <div id="menu">
33
+
34
+ <a href="_index.html">Index</a> &raquo;
35
+
36
+
37
+ <span class="title">Top Level Namespace</span>
38
+
39
+
40
+ <div class="noframes"><span class="title">(</span><a href="." target="_top">no frames</a><span class="title">)</span></div>
41
+ </div>
42
+
43
+ <div id="search">
44
+
45
+ <a class="full_list_link" id="class_list_link"
46
+ href="class_list.html">
47
+ Class List
48
+ </a>
49
+
50
+ <a class="full_list_link" id="method_list_link"
51
+ href="method_list.html">
52
+ Method List
53
+ </a>
54
+
55
+ <a class="full_list_link" id="file_list_link"
56
+ href="file_list.html">
57
+ File List
58
+ </a>
59
+
60
+ </div>
61
+ <div class="clear"></div>
62
+ </div>
63
+
64
+ <iframe id="search_frame"></iframe>
65
+
66
+ <div id="content"><h1>Top Level Namespace
67
+
68
+
69
+
70
+ </h1>
71
+
72
+ <dl class="box">
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+ </dl>
82
+ <div class="clear"></div>
83
+
84
+ <h2>Defined Under Namespace</h2>
85
+ <p class="children">
86
+
87
+
88
+ <strong class="modules">Modules:</strong> <span class='object_link'><a href="Isimud.html" title="Isimud (module)">Isimud</a></span>
89
+
90
+
91
+
92
+
93
+ </p>
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+ </div>
104
+
105
+ <div id="footer">
106
+ Generated on Fri Apr 3 09:28:08 2015 by
107
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
108
+ 0.8.7.6 (ruby-2.2.1).
109
+ </div>
110
+
111
+ </body>
112
+ </html>
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'isimud/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'isimud'
8
+ spec.version = Isimud::VERSION
9
+ spec.authors = ['George Feil', 'Brian Jenkins']
10
+ spec.email = %w{george.feil@keas.com bonkydog@bonkydog.com}
11
+ spec.summary = %q{AMQP Messaging for Events and ActiveRecord changes}
12
+ spec.description = %q{}
13
+ spec.homepage = ''
14
+ spec.license = "Copyright (C) 2014 Keas -- All rights reserved"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'activerecord', '>= 3.2.17'
22
+ spec.add_dependency 'activesupport', '>= 3.2.17'
23
+ spec.add_dependency 'bunny', '>= 1.6.0'
24
+ spec.add_dependency 'chronic_duration', '>= 0.10.6'
25
+ end
@@ -0,0 +1,91 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/module/attribute_accessors'
3
+
4
+ module Isimud
5
+ include ::ActiveSupport::Configurable
6
+ # @!attribute [r] client_type
7
+ # @return [Enumerable<'bunny', 'test'>] Type of client to use
8
+ # @!attribute [r] client_options
9
+ # @return [Hash] client specific options
10
+ # @!attribute [r] default_client
11
+ # @return [Isimud::Client] default client
12
+ # @!attribute [r] events_exchange
13
+ # @return [String] AMQP exchange used for publishing Event instances
14
+ # @!attribute [r] enable_model_watcher
15
+ # @return [Boolean] when set, send Isimud::ModelWatcher messages
16
+ # @!attribute [r] listener_error_limit
17
+ # @return [Integer] maximum number of exceptions allowed per hour before listener shuts down (100)
18
+ # @!attribute [r] logger
19
+ # @return [Logger] logger for tracing messages (Rails.logger)
20
+ # @!attribute [r] log_level
21
+ # @return [Symbol] log level (:debug)
22
+ # @!attribute [r] model_watcher_exchange
23
+ # @return [String] AMQP exchange used for publishing ModelWatcher messages
24
+ # @!attribute [r] model_watcher_schema
25
+ # @return [String] schema name (Rails.configuration.database_configuration[Rails.env]['database'])
26
+ # @!attribute [r] prefetch_count
27
+ # @return [Integer] number of messages to fetch -- only applies to BunnyClient
28
+ # @!attribute [r] retry_failures
29
+ # @return [Boolean] when set, if an exception occurs during message processing, requeue it
30
+ # @!attribute [r] server
31
+ # @return [<String, Hash>] server connection attributes
32
+ config_accessor :client_type do
33
+ :bunny
34
+ end
35
+ config_accessor :client_options, :default_client, :enable_model_watcher, :model_watcher_schema, :retry_failures
36
+ config_accessor :listener_error_limit do
37
+ 100
38
+ end
39
+ config_accessor :logger do
40
+ Rails.logger
41
+ end
42
+ config_accessor :log_level do
43
+ :debug
44
+ end
45
+ config_accessor :events_exchange do
46
+ 'events'
47
+ end
48
+ config_accessor :model_watcher_exchange do
49
+ 'models'
50
+ end
51
+ config_accessor :prefetch_count do
52
+ 100
53
+ end
54
+ config_accessor :server do
55
+ ENV['AMQP_URL']
56
+ end
57
+
58
+ def self.client_class
59
+ type = "#{client_type}_client".classify
60
+ "Isimud::#{type}".constantize
61
+ end
62
+
63
+ # Fetch or initialize the messaging client for this process.
64
+ # @return [Isimud::Client] messaging client
65
+ def self.client
66
+ self.default_client ||= client_class.new(server, client_options || {})
67
+ end
68
+
69
+ # Connect to the messaging server
70
+ def self.connect
71
+ client.connect
72
+ end
73
+
74
+ # Return status of model watching mode
75
+ def self.model_watcher_enabled?
76
+ enable_model_watcher.nil? || enable_model_watcher
77
+ end
78
+
79
+ # Reconnect the messaging client
80
+ def self.reconnect
81
+ client.reconnect
82
+ end
83
+ end
84
+
85
+ require 'isimud/logging'
86
+ require 'isimud/client'
87
+ require 'isimud/event'
88
+ require 'isimud/event_listener'
89
+ require 'isimud/event_observer'
90
+ require 'isimud/model_watcher'
91
+ require 'isimud/version'
@@ -0,0 +1,95 @@
1
+ require 'bunny'
2
+ require 'logger'
3
+
4
+ module Isimud
5
+ class BunnyClient < Isimud::Client
6
+ DEFAULT_URL = 'amqp://guest:guest@localhost'
7
+
8
+ attr_reader :url
9
+
10
+ def initialize(_url = nil, _bunny_options = {})
11
+ log "Isimud::BunnyClient.initialize: options = #{_bunny_options.inspect}"
12
+ @url = _url || DEFAULT_URL
13
+ @bunny_options = _bunny_options
14
+ end
15
+
16
+ def bind(queue_name, exchange_name, *routing_keys, &block)
17
+ create_queue(queue_name, exchange_name,
18
+ queue_options: {durable: true},
19
+ routing_keys: routing_keys,
20
+ subscribe_options: {manual_ack: true}, &block)
21
+ end
22
+
23
+ def create_queue(queue_name, exchange_name, options = {}, &block)
24
+ queue_options = options[:queue_options] || {}
25
+ routing_keys = options[:routing_keys] || []
26
+ subscribe_options = options[:subscribe_options] || {}
27
+ log "Isimud: create_queue #{queue_name}: queue_options=#{queue_options.inspect} routing_keys=#{routing_keys.join(',')} subscribe_options=#{subscribe_options.inspect}"
28
+ current_channel = channel
29
+ queue = current_channel.queue(queue_name, queue_options)
30
+ routing_keys.each { |key| queue.bind(exchange_name, routing_key: key, nowait: false) }
31
+ queue.subscribe(subscribe_options) do |delivery_info, properties, payload|
32
+ begin
33
+ log "Isimud: queue #{queue_name} received #{delivery_info.delivery_tag} routing_key: #{delivery_info.routing_key}"
34
+ Thread.current['isimud_queue_name'] = queue_name
35
+ Thread.current['isimud_delivery_info'] = delivery_info
36
+ Thread.current['isimud_properties'] = properties
37
+ block.call(payload)
38
+ log "Isimud: queue #{queue_name} finished with #{delivery_info.delivery_tag}, acknowledging"
39
+ current_channel.ack(delivery_info.delivery_tag)
40
+ rescue => e
41
+ log("Isimud: queue #{queue_name} error processing #{delivery_info.delivery_tag} payload #{payload.inspect}: #{e.class.name} #{e.message}\n #{e.backtrace.join("\n ")}", :warn)
42
+ current_channel.reject(delivery_info.delivery_tag, Isimud.retry_failures)
43
+ exception_handler.try(:call, e)
44
+ end
45
+ end
46
+ queue
47
+ end
48
+
49
+ def delete_queue(queue_name)
50
+ channel.queue(queue_name).delete
51
+ end
52
+
53
+ def connection
54
+ @connection ||= ::Bunny.new(url, @bunny_options).tap(&:start)
55
+ end
56
+
57
+ alias connect connection
58
+
59
+ CHANNEL_KEY = :'isimud.bunny_client.channel'
60
+
61
+ def channel
62
+ if (channel = Thread.current[CHANNEL_KEY]).try(:open?)
63
+ channel
64
+ else
65
+ new_channel = connection.channel
66
+ new_channel.confirm_select
67
+ new_channel.prefetch(Isimud.prefetch_count) if Isimud.prefetch_count
68
+ Thread.current[CHANNEL_KEY] = new_channel
69
+ end
70
+ end
71
+
72
+ def reset
73
+ connection.close_all_channels
74
+ end
75
+
76
+ def connected?
77
+ @connection && @connection.open?
78
+ end
79
+
80
+ def close
81
+ connection.close
82
+ ensure
83
+ @connection = nil
84
+ end
85
+
86
+ def publish(exchange, routing_key, payload)
87
+ channel.topic(exchange, durable: true).publish(payload, routing_key: routing_key, persistent: true)
88
+ end
89
+
90
+ def reconnect
91
+ close
92
+ connect
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,48 @@
1
+ module Isimud
2
+ # @abstract Messaging queue service client
3
+ class Client
4
+ include Isimud::Logging
5
+
6
+ attr_reader :exception_handler
7
+
8
+ def initialize(server = nil, options = nil)
9
+ end
10
+
11
+ def bind(queue_name, exchange_name, *keys, &method)
12
+ end
13
+
14
+ def channel
15
+ end
16
+
17
+ def close
18
+ end
19
+
20
+ def connect
21
+ end
22
+
23
+ def connected?
24
+ end
25
+
26
+ def create_queue(queue_name, exchange_name, options = {}, &method)
27
+ end
28
+
29
+ def delete_queue(queue_name)
30
+ end
31
+
32
+ def on_exception(&block)
33
+ @exception_handler = block
34
+ end
35
+
36
+ def publish(exchange, routing_key, payload)
37
+ end
38
+
39
+ def reconnect
40
+ end
41
+
42
+ def reset
43
+ end
44
+ end
45
+ end
46
+
47
+ require 'isimud/bunny_client'
48
+ require 'isimud/test_client'
@@ -0,0 +1,112 @@
1
+ require 'active_support'
2
+
3
+ module Isimud
4
+ class Event
5
+ include Isimud::Logging
6
+ attr_accessor :type, :action, :user_id, :occurred_at, :eventful_type, :eventful_id, :attributes, :parameters
7
+ attr_writer :exchange
8
+
9
+ DEFAULT_TYPE = :model
10
+
11
+ # Initialize a new Event
12
+ # @overload new(user, eventful, attributes)
13
+ # @param[#id] user user associated by the event
14
+ # @param[ActiveRecord::Base] eventful object associated with event
15
+ # @param[Hash] parameters optional additional attributes
16
+ # @overload new(attributes)
17
+ # @param[Hash] attributes event attributes
18
+ # @option attributes [Integer] :user_id ID of User associated with event
19
+ # @option attributes [String] :eventful_type class of object associated with event
20
+ # @option attributes [Integer] :eventful_id id of object associated with event
21
+ # @option attributes [String] :exchange (Isimud.events_exchange) exchange for publishing event
22
+ # @option attributes [ActiveRecord::Base] :eventful object associated with event. This sets :eventful_type and :eventful_id.
23
+ # @option attributes [String, Symbol] :type (:model) event type
24
+ # @option attributes [String] :action event action
25
+ # @option attributes [Time] :occurred_at (Time.now) date and time event occurred
26
+ # @option attributes [Hash] :attributes event attributes
27
+ # @option attributes [Hash] :parameters additional parameters (deprecated)
28
+ def initialize(*args)
29
+ options = args.extract_options!.with_indifferent_access
30
+
31
+ self.type = options.delete(:type).try(:to_sym) || DEFAULT_TYPE
32
+ self.exchange = options.delete(:exchange)
33
+ self.action = options.delete(:action).try(:to_sym)
34
+ self.user_id = options.delete(:user_id)
35
+ self.occurred_at = if (occurred = options.delete(:occurred_at))
36
+ occurred.kind_of?(String) ? Time.parse(occurred) : occurred
37
+ else
38
+ Time.now.utc
39
+ end
40
+
41
+ eventful_object = options.delete(:eventful)
42
+
43
+ if args.length > 0
44
+ self.parameters = options
45
+ if (user = args.shift)
46
+ self.user_id = user.id
47
+ end
48
+ eventful_object ||= args.shift
49
+ end
50
+
51
+ if eventful_object
52
+ self.eventful_type = eventful_object.class.base_class.name
53
+ self.eventful_id = eventful_object.id
54
+ else
55
+ self.eventful_type = options.delete(:eventful_type)
56
+ self.eventful_id = options.delete(:eventful_id)
57
+ end
58
+ self.attributes = options.delete(:attributes)
59
+ self.parameters = options.delete(:parameters) || options
60
+ end
61
+
62
+ def exchange
63
+ @exchange || Isimud.events_exchange
64
+ end
65
+
66
+ def routing_key
67
+ [type.to_s, eventful_type, eventful_id, action].compact.join('.')
68
+ end
69
+
70
+ # Return hash of data to be serialized to JSON
71
+ # @option options [Boolean] :omit_parameters when set, do not include attributes or parameters in data
72
+ # @return [Hash] data to serialize
73
+ def as_json(options = {})
74
+ session_id = parameters.delete(:session_id) || Thread.current[:keas_session_id]
75
+
76
+ data = {type: type,
77
+ action: action,
78
+ user_id: user_id,
79
+ occurred_at: occurred_at,
80
+ eventful_type: eventful_type,
81
+ eventful_id: eventful_id,
82
+ session_id: session_id}
83
+ unless options[:omit_parameters]
84
+ data[:parameters] = parameters
85
+ data[:attributes] = attributes
86
+ end
87
+ data
88
+ end
89
+
90
+ def serialize
91
+ self.to_json
92
+ end
93
+
94
+ class << self
95
+ def parse(data)
96
+ Event.new(JSON.parse(data))
97
+ end
98
+
99
+ def publish(*args)
100
+ Event.new(*args).publish
101
+ end
102
+
103
+ alias_method :dispatch, :publish
104
+ end
105
+
106
+ def publish
107
+ data = self.serialize
108
+ log "Event#publish: #{self.inspect}"
109
+ Isimud.client.publish(exchange, routing_key, data)
110
+ end
111
+ end
112
+ end