euston-daemons 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/Gemfile +4 -0
  2. data/Gemfile.lock +96 -0
  3. data/LICENSE +19 -0
  4. data/Rakefile +161 -0
  5. data/euston-daemons.gemspec +72 -0
  6. data/lib/euston-daemons/command_processor_daemon/config/environment.rb +17 -0
  7. data/lib/euston-daemons/command_processor_daemon/lib/components/command_handler_component.rb +55 -0
  8. data/lib/euston-daemons/command_processor_daemon/lib/daemon.rb +43 -0
  9. data/lib/euston-daemons/command_processor_daemon/lib/settings.rb +19 -0
  10. data/lib/euston-daemons/command_processor_daemon/rake_task.rb +31 -0
  11. data/lib/euston-daemons/event_processor_daemon/config/environment.rb +23 -0
  12. data/lib/euston-daemons/event_processor_daemon/lib/components/event_handler_component.rb +72 -0
  13. data/lib/euston-daemons/event_processor_daemon/lib/daemon.rb +72 -0
  14. data/lib/euston-daemons/event_processor_daemon/lib/settings.rb +22 -0
  15. data/lib/euston-daemons/event_processor_daemon/rake_task.rb +37 -0
  16. data/lib/euston-daemons/framework/basic_component.rb +33 -0
  17. data/lib/euston-daemons/framework/component_shutdown.rb +22 -0
  18. data/lib/euston-daemons/framework/daemon.rb +27 -0
  19. data/lib/euston-daemons/framework/queue.rb +71 -0
  20. data/lib/euston-daemons/message_buffer_daemon/config/environment.rb +30 -0
  21. data/lib/euston-daemons/message_buffer_daemon/lib/components/buffer_component.rb +74 -0
  22. data/lib/euston-daemons/message_buffer_daemon/lib/components/event_store_component.rb +51 -0
  23. data/lib/euston-daemons/message_buffer_daemon/lib/daemon.rb +48 -0
  24. data/lib/euston-daemons/message_buffer_daemon/lib/message_logger.rb +54 -0
  25. data/lib/euston-daemons/message_buffer_daemon/lib/publisher.rb +56 -0
  26. data/lib/euston-daemons/message_buffer_daemon/lib/read_model/message_buffer.rb +54 -0
  27. data/lib/euston-daemons/message_buffer_daemon/lib/read_model/message_log.rb +36 -0
  28. data/lib/euston-daemons/message_buffer_daemon/lib/settings.rb +14 -0
  29. data/lib/euston-daemons/message_buffer_daemon/lib/subscriber.rb +60 -0
  30. data/lib/euston-daemons/message_buffer_daemon/rake_task.rb +30 -0
  31. data/lib/euston-daemons/rake_task.rb +116 -0
  32. data/lib/euston-daemons/version.rb +5 -0
  33. data/lib/euston-daemons.rb +17 -0
  34. metadata +201 -0
@@ -0,0 +1,22 @@
1
+ module Euston
2
+ module EventProcessorDaemon
3
+ module Settings
4
+ def self.configure(cfg=nil)
5
+ @config ||= {}
6
+ @config.merge!(cfg) if cfg && cfg.is_a?(Hash)
7
+ end
8
+ def self.server_instances
9
+ @config[:server_instances] || 2
10
+ end
11
+ def self.bus_port_base
12
+ (@config[:zmq_base_port] || 8200).to_i
13
+ end
14
+ def self.mongo_db_name
15
+ @config[:mongo_db_name]
16
+ end
17
+ def self.debug
18
+ @config[:debug]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ module Euston
2
+ class EventProcessorRakeTask < Euston::Daemons::RakeTask
3
+ attr_accessor :amqp_config_path, :daemon_config_path, :event_handler_namespaces, :i18n_locales_path, :mongoid_config_path
4
+
5
+ def initialize
6
+ @event_handler_namespaces = []
7
+ super(:event_processor_daemon)
8
+ end
9
+
10
+ def before_creating_task
11
+ @daemon_path = File.expand_path(File.dirname __FILE__) + File::SEPARATOR
12
+ @daemon_class = 'Euston::EventProcessorDaemon::Daemon'
13
+ end
14
+
15
+ def initialize_paths
16
+ EUSTON_LOG.debug "AMQP config path: #{@amqp_config_path}"
17
+ Object.const_set :AMQP_CONFIG_PATH, @amqp_config_path
18
+
19
+ EUSTON_LOG.debug "Daemon config path: #{@daemon_config_path}"
20
+ Object.const_set :DAEMON_CONFIG_PATH, @daemon_config_path
21
+
22
+ EUSTON_LOG.debug "Event handler namespaces: #{@event_handler_namespaces}"
23
+ Object.const_set :EVENT_HANDLER_NAMESPACES, @event_handler_namespaces
24
+
25
+ EUSTON_LOG.debug "i18n locales path: #{@i18n_locales_path}"
26
+ Object.const_set :I18N_LOCALES_PATH, @i18n_locales_path
27
+
28
+ EUSTON_LOG.debug "Mongoid config path: #{@mongoid_config_path}"
29
+ Object.const_set :MONGOID_CONFIG_PATH, @mongoid_config_path
30
+ end
31
+
32
+ def load_environment
33
+ EUSTON_LOG.debug "Loading environment"
34
+ require_rel 'config/environment.rb'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ module Euston
2
+ module Daemons
3
+ class BasicComponent
4
+ include Euston::Daemons::ComponentShutdown
5
+
6
+ attr_reader :client, :cli_thread
7
+
8
+ def initialize(hosted_object)
9
+ @client = hosted_object
10
+ end
11
+
12
+ def start
13
+ @cli_thread = Thread.new do
14
+ begin
15
+ @client.start
16
+ rescue => e
17
+ Thread.current[:exception] = e
18
+ end
19
+ check_exception_and_shutdown
20
+ end
21
+ end
22
+
23
+ def thread_state
24
+ @cli_thread.status
25
+ end
26
+
27
+ def stop
28
+ @cli_thread[:stop] = true
29
+ @client.disconnect if @client.respond_to?(:disconnect)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ module Euston
2
+ module Daemons
3
+ module ComponentShutdown
4
+ attr_writer :daemon
5
+
6
+ def check_exception_and_shutdown(errors=[])
7
+ if errors.compact.empty?
8
+ errors << Thread.current[:exception]
9
+ return if errors.compact.empty?
10
+ end
11
+ errors.compact.each do |e|
12
+ if e.respond_to?(:cause) #its a serious (Java?) exception
13
+ Thread.current[:stop] = true
14
+ @daemon.irrecoverable_signal_from_component! e
15
+ else
16
+ Safely.report! e
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ module Euston
2
+ module Daemon
3
+ def errors
4
+ @errors ||= Queue.new
5
+ end
6
+
7
+ def irrecoverable_signal_from_component!(e)
8
+ self.errors.push e
9
+ @queue.push "SHUTDOWN"
10
+ end
11
+
12
+ def report_shutdown_reasons
13
+ sleep(0.25)
14
+ reports = []
15
+ until self.errors.empty? do
16
+ e = self.errors.pop(true) rescue nil
17
+ next if e.nil?
18
+ case e
19
+ when NativeException
20
+ Safely.report!(e.cause)
21
+ else
22
+ Safely.report!(e)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,71 @@
1
+ class RabbitMQClient
2
+ class Queue
3
+ include Hollywood
4
+
5
+ attr_writer :timeout
6
+
7
+ def delivery_timeout
8
+ @timeout ||= 500
9
+ end
10
+
11
+ def safe_subscribe
12
+ _consumer = self.consumer
13
+ until Thread.current[:stop] do
14
+ safe_subscribe_with_timeout(_consumer, self.delivery_timeout)
15
+ end
16
+ end
17
+
18
+ def safe_handle_message(msg)
19
+ begin
20
+ message = JSON.parse_sym(msg.body)
21
+ begin
22
+ callback :message_received, message
23
+ msg.ack!
24
+ rescue Euston::EventStore::ConcurrencyError
25
+ msg.reject! true #requeue
26
+ rescue => e
27
+ callback :message_failed, message, e, msg
28
+ Safely.report! e
29
+ end
30
+ rescue => e
31
+ callback :message_decode_failed, msg.body, e
32
+ msg.ack!
33
+ Safely.report! e
34
+ end
35
+ end
36
+
37
+ def consumer(auto_ack=false)
38
+ consumer = QueueingConsumer.new(@channel)
39
+ @channel.basic_consume(@name, auto_ack, consumer)
40
+ consumer
41
+ end
42
+
43
+ def safe_subscribe_with_timeout(consumer, timeout=500)
44
+ loop do
45
+ delivery = nil
46
+ begin
47
+ delivery = consumer.next_delivery(timeout)
48
+ rescue NativeException => e
49
+ Thread.current[:exception] = e
50
+ break
51
+ end
52
+ break if delivery.nil?
53
+ remsg = ReactiveMessage.new(@channel, delivery, String.from_java_bytes(delivery.get_body))
54
+ safe_handle_message remsg
55
+ @channel.basic_ack(delivery.envelope.delivery_tag, false) if remsg.should_acknowledge?
56
+ end
57
+ end
58
+ end
59
+
60
+ class ReactiveMessage
61
+ def reject(opts={})
62
+ self.reject!(opts.fetch(:requeue, true))
63
+ end
64
+ def ack(multiple=false)
65
+ self.ack!
66
+ end
67
+ def method()
68
+ self.envelope
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,30 @@
1
+ require 'jessica'
2
+ require 'euston'
3
+ require 'euston-eventstore'
4
+ require 'euston-rabbitmq'
5
+
6
+ require_rel '../lib'
7
+
8
+ Safely::Strategy::Log.logger = EUSTON_LOG
9
+ AMQP.settings.merge! ErbYaml.read(AMQP_CONFIG_PATH, EUSTON_ENV)
10
+ Euston::MessageBufferDaemon::Settings.configure ErbYaml.read(DAEMON_CONFIG_PATH, EUSTON_ENV)
11
+
12
+ hash = ErbYaml.read(MONGOID_CONFIG_PATH, EUSTON_ENV)
13
+
14
+ event_connection = Mongo::Connection.new hash[:host], hash[:port], :safe => hash[:safe] #, :logger => EUSTON_LOG
15
+ read_connection = Mongo::Connection.new hash[:host], hash[:port], :safe => hash[:safe] #, :logger => EUSTON_LOG
16
+
17
+ event_database = hash[:event_store_database]
18
+
19
+ Euston::RabbitMq.event_store_mongodb = Mongo::DB.new event_database, event_connection
20
+ Euston::RabbitMq.read_model_mongodb = Mongo::DB.new hash[:read_model_database], read_connection
21
+
22
+ Euston::EventStore::Persistence::Mongodb::Config.instance.logger = EUSTON_LOG
23
+ Euston::EventStore::Persistence::Mongodb::Config.instance.database = event_database
24
+
25
+ Euston::RabbitMq.event_store = Euston::EventStore::Persistence::Mongodb::MongoPersistenceFactory.build
26
+ Euston::RabbitMq.event_store.init
27
+
28
+ Euston::Repository.event_store = Euston::EventStore::OptimisticEventStore.new Euston::RabbitMq.event_store
29
+
30
+
@@ -0,0 +1,74 @@
1
+ module Euston
2
+ module MessageBufferDaemon
3
+ class BufferComponent
4
+ include Euston::Daemons::ComponentShutdown
5
+
6
+ class << self
7
+ def command_component pub_channel, sub_channel
8
+ self.new Publisher.commands_buffer(pub_channel), Subscriber.commands_buffer(sub_channel)
9
+ end
10
+
11
+ def event_component pub_channel, sub_channel
12
+ self.new Publisher.events_buffer(pub_channel), Subscriber.events_buffer(sub_channel)
13
+ end
14
+ end
15
+
16
+ attr_reader :pub_thread, :sub_thread
17
+ attr_writer :wait_time, :subscribe_timeout
18
+
19
+ def initialize pub_buffer, sub_buffer
20
+ @pub_buffer = pub_buffer
21
+ @sub_buffer = sub_buffer
22
+ end
23
+
24
+ def wait_time
25
+ @wait_time ||= 0.2
26
+ end
27
+
28
+ def subscribe_timeout
29
+ @subscribe_timeout ||= 2000
30
+ end
31
+
32
+ def thread_state
33
+ {:publish => @pub_thread.status, :subscribe => @sub_thread.status}
34
+ end
35
+
36
+ def stop
37
+ @pub_thread[:stop], @sub_thread[:stop] = true, true
38
+ @pub_buffer.disconnect
39
+ @sub_buffer.disconnect
40
+ end
41
+
42
+ def start
43
+ @pub_thread = Thread.new do
44
+ until Thread.current[:stop] do
45
+ begin
46
+ @msg_dispatched_count = 0
47
+ begin
48
+ @msg_dispatched_count = @pub_buffer.dispatch_due_messages
49
+ check_exception_and_shutdown
50
+ end until Thread.current[:stop] || @msg_dispatched_count.zero?
51
+ rescue => e
52
+ Thread.current[:exception] = e
53
+ end
54
+ check_exception_and_shutdown
55
+ sleep self.wait_time
56
+ end
57
+ end
58
+
59
+ @sub_thread = Thread.new do
60
+ until Thread.current[:stop] do
61
+ begin
62
+ @sub_buffer.dequeue_buffered_messages(self.subscribe_timeout)
63
+ check_exception_and_shutdown
64
+ rescue => e
65
+ Thread.current[:exception] = e
66
+ end
67
+ check_exception_and_shutdown
68
+ end
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,51 @@
1
+ module Euston
2
+ module MessageBufferDaemon
3
+ class EventStoreComponent
4
+ include Euston::Daemons::ComponentShutdown
5
+
6
+ attr_reader :cli_thread
7
+ attr_writer :wait_time
8
+
9
+ def initialize channel
10
+ @channel = channel
11
+ end
12
+
13
+ def wait_time
14
+ @wait_time ||= 0.2
15
+ end
16
+
17
+ def start
18
+ @cli_thread = Thread.new do
19
+ @event_buffer = Euston::MessageBufferDaemon::ReadModel::MessageBuffer.events
20
+ @event_store = Euston::RabbitMq.event_store
21
+ until Thread.current[:stop] do
22
+ begin
23
+ loop do
24
+ commits = @event_store.get_undispatched_commits
25
+ break if commits.empty?
26
+ commits.each do |commit|
27
+ commit.events.each { |event| @event_buffer.buffer_new_message event.to_hash }
28
+ @event_store.mark_commit_as_dispatched commit
29
+ end
30
+ end
31
+ rescue => e
32
+ Thread.current[:exception] = e
33
+ end
34
+ check_exception_and_shutdown
35
+ sleep self.wait_time
36
+ end
37
+ end
38
+ end
39
+
40
+ def thread_state
41
+ @cli_thread.status
42
+ end
43
+
44
+ def stop
45
+ @cli_thread[:stop] = true
46
+ @channel.disconnect
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,48 @@
1
+ module Euston
2
+ module MessageBufferDaemon
3
+ class Daemon
4
+ include Euston::Daemon
5
+
6
+ attr_reader :clients, :queue
7
+
8
+ def initialize()
9
+ @queue = Queue.new
10
+
11
+ @clients = {
12
+ :commands_logger => Euston::Daemons::BasicComponent.new(MessageLogger.commands_logger AMQP::Channel.new),
13
+ :events_logger => Euston::Daemons::BasicComponent.new(MessageLogger.events_logger AMQP::Channel.new),
14
+ :command_buffer_handler => BufferComponent.command_component(AMQP::Channel.new, AMQP::Channel.new),
15
+ :event_buffer_handler => BufferComponent.event_component(AMQP::Channel.new, AMQP::Channel.new),
16
+ :event_store_dispatcher => EventStoreComponent.new(AMQP::Channel.new)
17
+ }
18
+ end
19
+
20
+ def run
21
+ Thread.abort_on_exception = true
22
+
23
+ @clients.each do |name, component|
24
+ EUSTON_LOG.debug("Starting component: #{name}")
25
+ component.daemon = self
26
+ component.start
27
+ sleep 0.350
28
+ end
29
+
30
+ @clients.each do |name, component|
31
+ EUSTON_LOG.debug("Thread state of #{name}: #{component.thread_state.inspect}")
32
+ end
33
+
34
+ EUSTON_LOG.debug "Components started"
35
+
36
+ @queue.pop #<-------- stops here until interrupted
37
+
38
+ @clients.each do |name, component|
39
+ EUSTON_LOG.debug "Stopping component: #{name}"
40
+ component.stop
41
+ end
42
+
43
+ report_shutdown_reasons
44
+ EUSTON_LOG.debug "Components stopped"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,54 @@
1
+ module Euston
2
+ class MessageLogger
3
+ class << self
4
+ def commands_logger channel
5
+ self.new channel, :commands
6
+ end
7
+
8
+ def events_logger channel
9
+ self.new channel, :events
10
+ end
11
+ end
12
+
13
+ include Euston::RabbitMq::Exchanges
14
+ include Euston::RabbitMq::Queues
15
+
16
+ def initialize channel, exchange_name
17
+ @channel = channel
18
+ @exchange = get_exchange channel, exchange_name
19
+ @read_model = Euston::MessageBufferDaemon::ReadModel::MessageLog.send exchange_name
20
+
21
+ @queue = get_queue channel, "#{exchange_name}_log"
22
+ @queue.bind @exchange, :routing_key => "#{exchange_name}.#"
23
+
24
+ @queue.when(:message_decode_failed => method(:log_failure),
25
+ :message_failed => method(:message_failed),
26
+ :message_received => method(:write_message_to_log))
27
+ end
28
+
29
+ def message_failed(message, error, header)
30
+ log_failure message, error
31
+ header.ack
32
+ end
33
+
34
+ def disconnect
35
+ @channel.disconnect
36
+ end
37
+
38
+ def start
39
+ @queue.safe_subscribe
40
+ end
41
+
42
+ def log_failure message, error
43
+ text = "A log queue subscription failed. [Error] #{error.message} [Payload] #{message}"
44
+ err = Euston::RabbitMq::MessageDecodeFailedError.new text
45
+ err.set_backtrace error.backtrace
46
+
47
+ Safely.report! err
48
+ end
49
+
50
+ def write_message_to_log message
51
+ @read_model.log_new_message message
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,56 @@
1
+ module Euston
2
+ module MessageBufferDaemon
3
+ class Publisher
4
+ class << self
5
+ def commands_buffer channel
6
+ self.new channel, :commands
7
+ end
8
+
9
+ def events_buffer channel
10
+ self.new channel, :events
11
+ end
12
+ end
13
+
14
+ include Euston::RabbitMq::Exchanges
15
+
16
+ def initialize channel, exchange_name
17
+ @read_model = Euston::MessageBufferDaemon::ReadModel::MessageBuffer.send exchange_name
18
+
19
+ @channel = channel
20
+ @exchange = get_exchange @channel, exchange_name
21
+ end
22
+
23
+ def disconnect
24
+ @channel.disconnect
25
+ end
26
+
27
+ def dispatch_one_due_message
28
+ if (message = @read_model.find_next_message)
29
+ @read_model.set_next_attempt message
30
+ protected_publish(message)
31
+ end
32
+ end
33
+
34
+ def dispatch_due_messages
35
+ dispatched = 0
36
+ @read_model.find_due_messages.each do |message|
37
+ @read_model.set_next_attempt message
38
+ break unless protected_publish(message)
39
+ dispatched += 1
40
+ end
41
+ dispatched
42
+ end
43
+
44
+ def protected_publish(message)
45
+ ret = true
46
+ begin
47
+ @exchange.publish message['json'], default_publish_options.merge(:routing_key => "#{@exchange.name}.#{message['type']}")
48
+ rescue NativeException => e
49
+ Thread.current[:exception] = e
50
+ ret = false
51
+ end
52
+ ret
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,54 @@
1
+ module Euston
2
+ module MessageBufferDaemon
3
+ module ReadModel
4
+ class MessageBuffer
5
+ class << self
6
+ def commands
7
+ self.new "buffered_commands", Euston::RabbitMq.read_model_mongodb
8
+ end
9
+
10
+ def events
11
+ self.new "buffered_events", Euston::RabbitMq.event_store_mongodb
12
+ end
13
+ end
14
+
15
+ def initialize name, mongodb
16
+ mongodb.create_collection name unless mongodb.collection_names.include? name
17
+
18
+ @collection = mongodb.collection name
19
+ @collection.ensure_index [ ['next_attempt', Mongo::ASCENDING] ], :unique => false, :name => "#{name}_next_attempt_index"
20
+ end
21
+
22
+ def buffer_new_message message
23
+ @collection.save({ 'doc_id' => message[:headers][:id],
24
+ 'type' => message[:headers][:type],
25
+ 'next_attempt' => (Time.now.to_f - 1.0),
26
+ 'json' => ActiveSupport::JSON.encode(message) }, :safe => { :fsync => true })
27
+ end
28
+
29
+ def find_next_message
30
+ query = { 'next_attempt' => { '$lte' => Time.now.to_f } }
31
+ @collection.find_one(query)
32
+ end
33
+
34
+ def find_due_messages
35
+ query = { 'next_attempt' => { '$lte' => Time.now.to_f }}
36
+ @collection.find(query)
37
+ end
38
+
39
+ def get_by_id id
40
+ @collection.find_one({ 'doc_id' => id }) #doc or nil
41
+ end
42
+
43
+ def set_next_attempt message
44
+ @collection.update({ 'doc_id' => message['doc_id'] },
45
+ { '$set' => { 'next_attempt' => Time.now.to_f + 10 } }, :safe => { :fsync => true })
46
+ end
47
+
48
+ def remove_published_message id
49
+ @collection.remove({ 'doc_id' => id }, :safe => { :fsync => true })
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,36 @@
1
+ module Euston
2
+ module MessageBufferDaemon
3
+ module ReadModel
4
+ class MessageLog
5
+ class << self
6
+ def commands
7
+ self.new "command_log", Euston::RabbitMq.read_model_mongodb
8
+ end
9
+
10
+ def events
11
+ self.new "event_log", Euston::RabbitMq.event_store_mongodb
12
+ end
13
+ end
14
+
15
+ def initialize name, mongodb
16
+ mongodb.create_collection name unless mongodb.collection_names.include? name
17
+
18
+ @collection = mongodb.collection name
19
+ @collection.ensure_index [ ['timestamp', Mongo::ASCENDING] ], :unique => false, :name => "#{name}_timestamp_index"
20
+ end
21
+
22
+ def find_all
23
+ @collection.find({}, { :sort => [ 'timestamp', Mongo::DESCENDING ] })
24
+ end
25
+
26
+ def log_new_message message
27
+ @collection.save({ '_id' => message[:headers][:id],
28
+ 'type' => message[:headers][:type],
29
+ 'version' => message[:headers][:version],
30
+ 'timestamp' => Time.now.to_f,
31
+ 'json' => ActiveSupport::JSON.encode(message) }, :safe => { :fsync => true })
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,14 @@
1
+ module Euston
2
+ module MessageBufferDaemon
3
+ module Settings
4
+ def self.configure(cfg=nil)
5
+ @config ||= {}
6
+ @config.merge!(cfg) if cfg && cfg.is_a?(Hash)
7
+ end
8
+
9
+ def self.debug
10
+ @config[:debug]
11
+ end
12
+ end
13
+ end
14
+ end