euston-daemons 1.0.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.
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