rabbit_feed 0.3.1
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 +11 -0
- data/.rspec +3 -0
- data/Brewfile +4 -0
- data/DEVELOPING.md +140 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +121 -0
- data/LICENSE.txt +9 -0
- data/README.md +304 -0
- data/Rakefile +30 -0
- data/bin/bundle +3 -0
- data/bin/rabbit_feed +11 -0
- data/example/non_rails_app/.rspec +1 -0
- data/example/non_rails_app/Gemfile +7 -0
- data/example/non_rails_app/Gemfile.lock +56 -0
- data/example/non_rails_app/Rakefile +5 -0
- data/example/non_rails_app/bin/benchmark +63 -0
- data/example/non_rails_app/bin/bundle +3 -0
- data/example/non_rails_app/config/rabbit_feed.yml +8 -0
- data/example/non_rails_app/lib/non_rails_app.rb +32 -0
- data/example/non_rails_app/lib/non_rails_app/event_handler.rb +10 -0
- data/example/non_rails_app/log/.keep +0 -0
- data/example/non_rails_app/spec/lib/non_rails_app/event_handler_spec.rb +14 -0
- data/example/non_rails_app/spec/lib/non_rails_app/event_routing_spec.rb +14 -0
- data/example/non_rails_app/spec/spec_helper.rb +31 -0
- data/example/non_rails_app/tmp/pids/.keep +0 -0
- data/example/rails_app/.gitignore +17 -0
- data/example/rails_app/.node-version +1 -0
- data/example/rails_app/.rspec +1 -0
- data/example/rails_app/Gemfile +36 -0
- data/example/rails_app/Gemfile.lock +173 -0
- data/example/rails_app/README.rdoc +28 -0
- data/example/rails_app/Rakefile +6 -0
- data/example/rails_app/app/assets/images/.keep +0 -0
- data/example/rails_app/app/assets/javascripts/application.js +16 -0
- data/example/rails_app/app/assets/javascripts/beavers.js.coffee +3 -0
- data/example/rails_app/app/assets/stylesheets/application.css +15 -0
- data/example/rails_app/app/assets/stylesheets/beavers.css.scss +3 -0
- data/example/rails_app/app/assets/stylesheets/scaffolds.css.scss +69 -0
- data/example/rails_app/app/controllers/application_controller.rb +5 -0
- data/example/rails_app/app/controllers/beavers_controller.rb +81 -0
- data/example/rails_app/app/controllers/concerns/.keep +0 -0
- data/example/rails_app/app/helpers/application_helper.rb +2 -0
- data/example/rails_app/app/helpers/beavers_helper.rb +2 -0
- data/example/rails_app/app/mailers/.keep +0 -0
- data/example/rails_app/app/models/.keep +0 -0
- data/example/rails_app/app/models/beaver.rb +2 -0
- data/example/rails_app/app/models/concerns/.keep +0 -0
- data/example/rails_app/app/views/beavers/_form.html.erb +21 -0
- data/example/rails_app/app/views/beavers/edit.html.erb +6 -0
- data/example/rails_app/app/views/beavers/index.html.erb +25 -0
- data/example/rails_app/app/views/beavers/index.json.jbuilder +4 -0
- data/example/rails_app/app/views/beavers/new.html.erb +5 -0
- data/example/rails_app/app/views/beavers/show.html.erb +9 -0
- data/example/rails_app/app/views/beavers/show.json.jbuilder +1 -0
- data/example/rails_app/app/views/layouts/application.html.erb +14 -0
- data/example/rails_app/bin/bundle +3 -0
- data/example/rails_app/bin/rails +4 -0
- data/example/rails_app/bin/rake +4 -0
- data/example/rails_app/config.ru +4 -0
- data/example/rails_app/config/application.rb +25 -0
- data/example/rails_app/config/boot.rb +4 -0
- data/example/rails_app/config/database.yml +22 -0
- data/example/rails_app/config/environment.rb +5 -0
- data/example/rails_app/config/environments/development.rb +83 -0
- data/example/rails_app/config/environments/test.rb +39 -0
- data/example/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/example/rails_app/config/initializers/cookies_serializer.rb +3 -0
- data/example/rails_app/config/initializers/filter_parameter_logging.rb +4 -0
- data/example/rails_app/config/initializers/inflections.rb +16 -0
- data/example/rails_app/config/initializers/mime_types.rb +4 -0
- data/example/rails_app/config/initializers/rabbit_feed.rb +43 -0
- data/example/rails_app/config/initializers/session_store.rb +3 -0
- data/example/rails_app/config/initializers/wrap_parameters.rb +14 -0
- data/example/rails_app/config/locales/en.yml +23 -0
- data/example/rails_app/config/rabbit_feed.yml +8 -0
- data/example/rails_app/config/routes.rb +58 -0
- data/example/rails_app/config/secrets.yml +18 -0
- data/example/rails_app/config/unicorn.rb +4 -0
- data/example/rails_app/db/migrate/20140424102400_create_beavers.rb +9 -0
- data/example/rails_app/db/schema.rb +22 -0
- data/example/rails_app/db/seeds.rb +7 -0
- data/example/rails_app/lib/assets/.keep +0 -0
- data/example/rails_app/lib/event_handler.rb +7 -0
- data/example/rails_app/lib/tasks/.keep +0 -0
- data/example/rails_app/log/.keep +0 -0
- data/example/rails_app/public/404.html +67 -0
- data/example/rails_app/public/422.html +67 -0
- data/example/rails_app/public/500.html +66 -0
- data/example/rails_app/public/favicon.ico +0 -0
- data/example/rails_app/public/robots.txt +5 -0
- data/example/rails_app/spec/controllers/beavers_controller_spec.rb +32 -0
- data/example/rails_app/spec/event_routing_spec.rb +15 -0
- data/example/rails_app/spec/spec_helper.rb +51 -0
- data/example/rails_app/test/controllers/.keep +0 -0
- data/example/rails_app/test/controllers/beavers_controller_test.rb +49 -0
- data/example/rails_app/test/fixtures/.keep +0 -0
- data/example/rails_app/test/fixtures/beavers.yml +7 -0
- data/example/rails_app/test/helpers/.keep +0 -0
- data/example/rails_app/test/helpers/beavers_helper_test.rb +4 -0
- data/example/rails_app/test/integration/.keep +0 -0
- data/example/rails_app/test/mailers/.keep +0 -0
- data/example/rails_app/test/models/.keep +0 -0
- data/example/rails_app/test/models/beaver_test.rb +7 -0
- data/example/rails_app/test/test_helper.rb +13 -0
- data/example/rails_app/tmp/pids/.keep +0 -0
- data/example/rails_app/vendor/assets/javascripts/.keep +0 -0
- data/example/rails_app/vendor/assets/stylesheets/.keep +0 -0
- data/lib/dsl.rb +9 -0
- data/lib/rabbit_feed.rb +41 -0
- data/lib/rabbit_feed/client.rb +181 -0
- data/lib/rabbit_feed/configuration.rb +50 -0
- data/lib/rabbit_feed/connection_concern.rb +95 -0
- data/lib/rabbit_feed/consumer.rb +14 -0
- data/lib/rabbit_feed/consumer_connection.rb +108 -0
- data/lib/rabbit_feed/event.rb +43 -0
- data/lib/rabbit_feed/event_definitions.rb +98 -0
- data/lib/rabbit_feed/event_routing.rb +90 -0
- data/lib/rabbit_feed/producer.rb +47 -0
- data/lib/rabbit_feed/producer_connection.rb +65 -0
- data/lib/rabbit_feed/testing_support/rspec_matchers/publish_event.rb +90 -0
- data/lib/rabbit_feed/testing_support/testing_helpers.rb +16 -0
- data/lib/rabbit_feed/version.rb +3 -0
- data/logo.png +0 -0
- data/rabbit_feed.gemspec +35 -0
- data/run_benchmark +35 -0
- data/run_example +62 -0
- data/run_recovery_test +26 -0
- data/spec/features/connectivity.feature +13 -0
- data/spec/features/step_definitions/connectivity_steps.rb +96 -0
- data/spec/fixtures/configuration.yml +14 -0
- data/spec/lib/rabbit_feed/client_spec.rb +116 -0
- data/spec/lib/rabbit_feed/configuration_spec.rb +121 -0
- data/spec/lib/rabbit_feed/connection_concern_spec.rb +116 -0
- data/spec/lib/rabbit_feed/consumer_connection_spec.rb +85 -0
- data/spec/lib/rabbit_feed/event_definitions_spec.rb +139 -0
- data/spec/lib/rabbit_feed/event_routing_spec.rb +121 -0
- data/spec/lib/rabbit_feed/event_spec.rb +33 -0
- data/spec/lib/rabbit_feed/producer_connection_spec.rb +72 -0
- data/spec/lib/rabbit_feed/producer_spec.rb +57 -0
- data/spec/lib/rabbit_feed/testing_support/rspec_matchers/publish_event_spec.rb +60 -0
- data/spec/lib/rabbit_feed/testing_support/testing_helper_spec.rb +34 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/support/shared_examples_for_connections.rb +40 -0
- metadata +305 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module RabbitFeed
|
|
2
|
+
class Configuration
|
|
3
|
+
include ActiveModel::Validations
|
|
4
|
+
|
|
5
|
+
attr_reader :host, :port, :user, :password, :application, :environment, :exchange, :pool_size, :pool_timeout, :heartbeat, :connect_timeout, :network_recovery_interval, :auto_delete_queue, :auto_delete_exchange
|
|
6
|
+
validates_presence_of :host, :port, :user, :password, :application, :environment, :exchange, :pool_size, :pool_timeout, :heartbeat, :connect_timeout, :network_recovery_interval
|
|
7
|
+
|
|
8
|
+
def initialize options
|
|
9
|
+
RabbitFeed.log.debug "RabbitFeed initialising with options: #{options}..."
|
|
10
|
+
|
|
11
|
+
@host = options[:host] || 'localhost'
|
|
12
|
+
@port = options[:port] || 5672
|
|
13
|
+
@user = options[:user] || 'guest'
|
|
14
|
+
@password = options[:password] || 'guest'
|
|
15
|
+
@exchange = options[:exchange] || 'amq.topic'
|
|
16
|
+
@pool_size = options[:pool_size] || 1
|
|
17
|
+
@pool_timeout = options[:pool_timeout] || 5
|
|
18
|
+
@heartbeat = options[:heartbeat] || 5
|
|
19
|
+
@connect_timeout = options[:connect_timeout] || 10
|
|
20
|
+
@network_recovery_interval = options[:network_recovery_interval] || 1
|
|
21
|
+
@auto_delete_queue = !!(options[:auto_delete_queue] || false)
|
|
22
|
+
@auto_delete_exchange = !!(options[:auto_delete_exchange] || false)
|
|
23
|
+
@application = options[:application]
|
|
24
|
+
@environment = options[:environment]
|
|
25
|
+
validate!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.load file_path, environment
|
|
29
|
+
RabbitFeed.log.debug "Reading configurations from #{file_path} in #{environment}..."
|
|
30
|
+
|
|
31
|
+
options = read_configuration_file file_path, environment
|
|
32
|
+
new options.merge(environment: environment)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def queue
|
|
36
|
+
"#{environment}.#{application}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def self.read_configuration_file file_path, environment
|
|
42
|
+
raw_configuration = YAML.load(ERB.new(File.read(file_path)).result)
|
|
43
|
+
HashWithIndifferentAccess.new (raw_configuration[environment] || {})
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate!
|
|
47
|
+
raise ConfigurationError.new errors.messages if invalid?
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module RabbitFeed
|
|
2
|
+
module ConnectionConcern
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
module ClassMethods
|
|
6
|
+
|
|
7
|
+
def default_connection_options
|
|
8
|
+
{
|
|
9
|
+
heartbeat: RabbitFeed.configuration.heartbeat,
|
|
10
|
+
connect_timeout: RabbitFeed.configuration.connect_timeout,
|
|
11
|
+
host: RabbitFeed.configuration.host,
|
|
12
|
+
user: RabbitFeed.configuration.user,
|
|
13
|
+
password: RabbitFeed.configuration.password,
|
|
14
|
+
port: RabbitFeed.configuration.port,
|
|
15
|
+
network_recovery_interval: RabbitFeed.configuration.network_recovery_interval,
|
|
16
|
+
logger: RabbitFeed.log,
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def with_connection &block
|
|
21
|
+
connection_pool.with do |connection|
|
|
22
|
+
yield connection
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def retry_on_exception tries=3, &block
|
|
27
|
+
yield
|
|
28
|
+
rescue Bunny::ConnectionClosedError
|
|
29
|
+
raise # There is no point in retrying if the connection is closed
|
|
30
|
+
rescue => e
|
|
31
|
+
RabbitFeed.log.warn "Exception encountered; #{tries - 1} tries remaining. #{self.to_s}: #{e.message} #{e.backtrace}"
|
|
32
|
+
unless (tries -= 1).zero?
|
|
33
|
+
retry
|
|
34
|
+
end
|
|
35
|
+
raise
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def retry_on_closed_connection tries=3, &block
|
|
39
|
+
yield
|
|
40
|
+
rescue Bunny::ConnectionClosedError => e
|
|
41
|
+
RabbitFeed.log.warn "Closed connection exception encountered; #{tries - 1} tries remaining. #{self.to_s}: #{e.message} #{e.backtrace}"
|
|
42
|
+
unless (tries -= 1).zero?
|
|
43
|
+
unset_connection
|
|
44
|
+
sleep RabbitFeed.configuration.network_recovery_interval
|
|
45
|
+
retry
|
|
46
|
+
end
|
|
47
|
+
raise
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def close
|
|
51
|
+
RabbitFeed.log.debug "Closing connection: #{self.to_s}..."
|
|
52
|
+
@bunny_connection.close if @bunny_connection.present? && !closed?
|
|
53
|
+
unset_connection
|
|
54
|
+
rescue => e
|
|
55
|
+
RabbitFeed.log.warn "Exception encountered whilst closing #{self.to_s}: #{e.message} #{e.backtrace}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def bunny_connection
|
|
59
|
+
if @bunny_connection.nil?
|
|
60
|
+
retry_on_exception do
|
|
61
|
+
RabbitFeed.log.debug "Opening connection: #{self.to_s}..."
|
|
62
|
+
@bunny_connection = Bunny.new connection_options
|
|
63
|
+
@bunny_connection.start
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@bunny_connection
|
|
68
|
+
end
|
|
69
|
+
private :bunny_connection
|
|
70
|
+
|
|
71
|
+
def connection_pool
|
|
72
|
+
@connection_pool ||= ConnectionPool.new(
|
|
73
|
+
size: RabbitFeed.configuration.pool_size,
|
|
74
|
+
timeout: RabbitFeed.configuration.pool_timeout
|
|
75
|
+
) do
|
|
76
|
+
new bunny_connection.create_channel
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
private :connection_pool
|
|
80
|
+
|
|
81
|
+
def closed?
|
|
82
|
+
@bunny_connection.present? && @bunny_connection.closed?
|
|
83
|
+
end
|
|
84
|
+
private :closed?
|
|
85
|
+
|
|
86
|
+
def unset_connection
|
|
87
|
+
RabbitFeed.log.debug "Unsetting connection: #{self.to_s}..."
|
|
88
|
+
@connection_pool = nil
|
|
89
|
+
@bunny_connection = nil
|
|
90
|
+
end
|
|
91
|
+
private :unset_connection
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module RabbitFeed
|
|
2
|
+
class ConsumerConnection
|
|
3
|
+
include ConnectionConcern
|
|
4
|
+
|
|
5
|
+
SUBSCRIPTION_OPTIONS = {
|
|
6
|
+
consumer_tag: Socket.gethostname, # Use the host name of the server
|
|
7
|
+
manual_ack: true, # Manually acknowledge messages once they've been processed
|
|
8
|
+
block: false, # Don't block the thread whilst consuming from the queue, as this breaks during connection recovery
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
SEVEN_DAYS_IN_MS = 7.days * 1000
|
|
12
|
+
|
|
13
|
+
QUEUE_OPTIONS = {
|
|
14
|
+
durable: true, # Persist across server restart
|
|
15
|
+
no_declare: false, # Create the queue if it does not exist
|
|
16
|
+
arguments: {
|
|
17
|
+
'x-ha-policy' => 'all', # Apply the queue on all mirrors
|
|
18
|
+
'x-expires' => SEVEN_DAYS_IN_MS, # Auto-delete the queue after a period of inactivity (in ms)
|
|
19
|
+
},
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :queue
|
|
23
|
+
|
|
24
|
+
def initialize channel
|
|
25
|
+
channel.prefetch(1) # Fetch one message at a time to preserve order
|
|
26
|
+
RabbitFeed.log.debug "Declaring queue on #{self.to_s} (channel #{channel.id}) named: #{RabbitFeed.configuration.queue} with options: #{queue_options}..."
|
|
27
|
+
@queue = channel.queue RabbitFeed.configuration.queue, queue_options
|
|
28
|
+
bind_on_accepted_routes
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.consume &block
|
|
32
|
+
with_connection do |consumer_connection|
|
|
33
|
+
consumer_connection.consume(&block)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def consume &block
|
|
38
|
+
RabbitFeed.log.info "Consuming messages on #{self.to_s} from queue: #{RabbitFeed.configuration.queue}..."
|
|
39
|
+
|
|
40
|
+
consumer = queue.subscribe(SUBSCRIPTION_OPTIONS) do |delivery_info, properties, payload|
|
|
41
|
+
handle_message delivery_info, payload, &block
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sleep # Sleep indefinitely, as the consumer runs in its own thread
|
|
45
|
+
rescue
|
|
46
|
+
(cancel_consumer consumer) if consumer.present?
|
|
47
|
+
raise
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def queue_options
|
|
53
|
+
{
|
|
54
|
+
auto_delete: RabbitFeed.configuration.auto_delete_queue,
|
|
55
|
+
}.merge QUEUE_OPTIONS
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.connection_options
|
|
59
|
+
default_connection_options.merge({
|
|
60
|
+
threaded: true,
|
|
61
|
+
})
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def bind_on_accepted_routes
|
|
65
|
+
if RabbitFeed::Consumer.event_routing.present?
|
|
66
|
+
RabbitFeed::Consumer.event_routing.accepted_routes.each do |accepted_route|
|
|
67
|
+
queue.bind(RabbitFeed.configuration.exchange, { routing_key: accepted_route })
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
queue.bind(RabbitFeed.configuration.exchange)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def acknowledge delivery_info
|
|
75
|
+
queue.channel.ack(delivery_info.delivery_tag)
|
|
76
|
+
RabbitFeed.log.debug "Message acknowledged on #{self.to_s} from queue: #{RabbitFeed.configuration.queue}..."
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def handle_message delivery_info, payload, &block
|
|
80
|
+
RabbitFeed.log.debug "Message received on #{self.to_s} from queue: #{RabbitFeed.configuration.queue}..."
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
yield payload
|
|
84
|
+
acknowledge delivery_info
|
|
85
|
+
rescue => e
|
|
86
|
+
handle_processing_exception delivery_info, e
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def cancel_consumer consumer
|
|
91
|
+
cancel_ok = consumer.cancel
|
|
92
|
+
RabbitFeed.log.debug "Consumer: #{cancel_ok.consumer_tag} cancelled on #{self.to_s} from queue: #{RabbitFeed.configuration.queue}..."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def negative_acknowledge delivery_info
|
|
96
|
+
# Tell rabbit that we were unable to process the message
|
|
97
|
+
# This will re-queue the message
|
|
98
|
+
queue.channel.nack(delivery_info.delivery_tag, false, true)
|
|
99
|
+
RabbitFeed.log.debug "Message negatively acknowledged on #{self.to_s} from queue: #{RabbitFeed.configuration.queue}..."
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def handle_processing_exception delivery_info, exception
|
|
103
|
+
negative_acknowledge delivery_info
|
|
104
|
+
RabbitFeed.log.error "Exception encountered while consuming message on #{self.to_s} from queue #{RabbitFeed.configuration.queue}: #{exception.message} #{exception.backtrace}"
|
|
105
|
+
RabbitFeed.exception_notify exception
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module RabbitFeed
|
|
2
|
+
class Event
|
|
3
|
+
include ActiveModel::Validations
|
|
4
|
+
|
|
5
|
+
attr_reader :schema, :payload
|
|
6
|
+
validates_presence_of :schema, :payload
|
|
7
|
+
|
|
8
|
+
def initialize schema, payload
|
|
9
|
+
@schema = schema
|
|
10
|
+
@payload = payload
|
|
11
|
+
validate!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def serialize
|
|
15
|
+
buffer = StringIO.new
|
|
16
|
+
writer = Avro::DataFile::Writer.new buffer, (Avro::IO::DatumWriter.new schema), schema
|
|
17
|
+
writer << payload
|
|
18
|
+
writer.close
|
|
19
|
+
buffer.string
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.deserialize event
|
|
23
|
+
datum_reader = Avro::IO::DatumReader.new
|
|
24
|
+
reader = Avro::DataFile::Reader.new (StringIO.new event), datum_reader
|
|
25
|
+
payload = nil
|
|
26
|
+
reader.each do |datum|
|
|
27
|
+
payload = datum
|
|
28
|
+
end
|
|
29
|
+
reader.close
|
|
30
|
+
Event.new datum_reader.readers_schema, payload
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def method_missing(method_name, *args, &block)
|
|
34
|
+
payload[method_name.to_s]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def validate!
|
|
40
|
+
raise Error.new errors.messages if invalid?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
module RabbitFeed
|
|
2
|
+
class EventDefinitions
|
|
3
|
+
|
|
4
|
+
class Field
|
|
5
|
+
include ActiveModel::Validations
|
|
6
|
+
|
|
7
|
+
attr_reader :name, :type, :definition
|
|
8
|
+
validates_presence_of :name, :type, :definition
|
|
9
|
+
|
|
10
|
+
def initialize name, type, definition
|
|
11
|
+
@name = name
|
|
12
|
+
@type = type
|
|
13
|
+
@definition = definition
|
|
14
|
+
validate!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def schema
|
|
18
|
+
{ name: name, type: type, doc: definition }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def validate!
|
|
24
|
+
raise ConfigurationError.new "Bad field specification for #{name}: #{errors.messages}" if invalid?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Event
|
|
29
|
+
include ActiveModel::Validations
|
|
30
|
+
|
|
31
|
+
attr_reader :name, :definition, :version, :fields
|
|
32
|
+
validates_presence_of :name, :definition, :version
|
|
33
|
+
validate :schema_parseable
|
|
34
|
+
validates :version, format: { with: /\A\d+\.\d+\.\d+\z/, message: 'must be in *.*.* format' }
|
|
35
|
+
|
|
36
|
+
def initialize name, version
|
|
37
|
+
@name = name
|
|
38
|
+
@version = version
|
|
39
|
+
@fields = []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def payload_contains &block
|
|
43
|
+
self.instance_eval(&block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def field name, options
|
|
47
|
+
fields << (Field.new name, options[:type], options[:definition])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def defined_as &block
|
|
51
|
+
@definition = block.call if block.present?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def payload
|
|
55
|
+
([
|
|
56
|
+
(Field.new 'application', 'string', 'The name of the application that created the event'),
|
|
57
|
+
(Field.new 'host', 'string', 'The hostname of the server on which the event was created'),
|
|
58
|
+
(Field.new 'environment', 'string', 'The environment in which the event was created'),
|
|
59
|
+
(Field.new 'version', 'string', 'The version of the event'),
|
|
60
|
+
(Field.new 'name', 'string', 'The name of the event'),
|
|
61
|
+
(Field.new 'created_at_utc', 'string', 'The UTC time that the event was created')
|
|
62
|
+
] + fields).map(&:schema)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def schema
|
|
66
|
+
@schema ||= (Avro::Schema.parse ({ name: name, type: 'record', doc: definition, fields: payload }.to_json))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate!
|
|
70
|
+
raise ConfigurationError.new "Bad event specification for #{name}: #{errors.messages}" if invalid?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def schema_parseable
|
|
76
|
+
schema
|
|
77
|
+
rescue => e
|
|
78
|
+
errors.add(:fields, "could not be parsed into a schema, reason: #{e.message}")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
attr_reader :events
|
|
83
|
+
|
|
84
|
+
def initialize
|
|
85
|
+
@events = {}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def define_event name, options, &block
|
|
89
|
+
events[name] = Event.new name, options[:version]
|
|
90
|
+
events[name].instance_eval(&block)
|
|
91
|
+
events[name].validate!
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def [] name
|
|
95
|
+
events[name]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module RabbitFeed
|
|
2
|
+
class EventRouting
|
|
3
|
+
|
|
4
|
+
class Event
|
|
5
|
+
include ActiveModel::Validations
|
|
6
|
+
|
|
7
|
+
attr_reader :name, :action
|
|
8
|
+
validates_presence_of :name, :action
|
|
9
|
+
validate :action_arity
|
|
10
|
+
|
|
11
|
+
def initialize name, block
|
|
12
|
+
@name = name
|
|
13
|
+
@action = block
|
|
14
|
+
|
|
15
|
+
validate!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def handle_event event
|
|
19
|
+
action.call event
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def action_arity
|
|
25
|
+
errors.add(:action, 'arity should be 1') if action.present? && action.arity != 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def validate!
|
|
29
|
+
raise ConfigurationError.new "Bad event specification for #{name}: #{errors.messages}" if invalid?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Application
|
|
34
|
+
include ActiveModel::Validations
|
|
35
|
+
|
|
36
|
+
attr_reader :events, :name
|
|
37
|
+
validates_presence_of :name
|
|
38
|
+
|
|
39
|
+
def initialize name
|
|
40
|
+
@name = name
|
|
41
|
+
@events = {}
|
|
42
|
+
|
|
43
|
+
validate!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def event name, &block
|
|
47
|
+
event = (Event.new name, block)
|
|
48
|
+
events[event.name] = event
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def accepted_routes
|
|
52
|
+
events.values.map do |event|
|
|
53
|
+
"#{RabbitFeed.environment}.#{name}.#{event.name}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def handle_event event
|
|
58
|
+
event_rule = events[event.name]
|
|
59
|
+
raise RoutingError.new "No routing defined for application with name: #{event.application} for events named: #{event.name}" unless event_rule.present?
|
|
60
|
+
event_rule.handle_event event
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validate!
|
|
64
|
+
raise ConfigurationError.new "Bad application specification for #{name}: #{errors.messages}" if invalid?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
attr_reader :applications
|
|
69
|
+
|
|
70
|
+
def initialize
|
|
71
|
+
@applications = {}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def accept_from name, &block
|
|
75
|
+
application = Application.new name
|
|
76
|
+
application.instance_eval(&block)
|
|
77
|
+
applications[application.name] = application
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def accepted_routes
|
|
81
|
+
applications.values.map{|application| application.accepted_routes }.flatten
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def handle_event event
|
|
85
|
+
application = applications[event.application]
|
|
86
|
+
raise RoutingError.new "No routing defined for application with name: #{event.application}" unless application.present?
|
|
87
|
+
application.handle_event event
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|