rabbit_feed 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|