philotic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,79 @@
1
+ require 'yaml'
2
+ require 'json'
3
+ require 'singleton'
4
+ require 'forwardable'
5
+
6
+ module Philotic
7
+ module Config
8
+ extend self
9
+
10
+ ENV_PREFIX = 'PHILOTIC'
11
+
12
+ DEFAULT_DISABLE_PUBLISH = false
13
+
14
+
15
+ DEFAULT_RABBIT_HOST = 'localhost'
16
+ DEFAULT_RABBIT_PORT = 5672
17
+ DEFAULT_RABBIT_USER = 'guest'
18
+ DEFAULT_RABBIT_PASSWORD = 'guest'
19
+ DEFAULT_RABBIT_VHOST = '/'
20
+ DEFAULT_EXCHANGE_NAME = 'philotic.headers'
21
+ DEFAULT_CONNECTION_FAILED_HANDLER = Proc.new { |settings| Philotic.logger.error "RabbitMQ connection failure; host:#{rabbit_host}" }
22
+ DEFAULT_CONNECTION_LOSS_HANDLER = Proc.new { |conn, settings| Philotic.logger.warn "RabbitMQ connection loss; host:#{rabbit_host}"; conn.reconnect(false, 2) }
23
+ DEFAULT_MESSAGE_RETURN_HANDLER = Proc.new { |basic_return, metadata, payload| Philotic.logger.warn "Philotic message #{JSON.parse payload} was returned! reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text} headers = #{metadata.properties}"; }
24
+ DEFAULT_TIMEOUT = 2
25
+
26
+ DEFAULT_ROUTING_KEY = nil
27
+ DEFAULT_PERSISTENT = true
28
+ # DEFAULT_IMMEDIATE = false
29
+ DEFAULT_MANDATORY = true
30
+ DEFAULT_CONTENT_TYPE = nil
31
+ DEFAULT_CONTENT_ENCODING = nil
32
+ DEFAULT_PRIORITY = nil
33
+ DEFAULT_MESSAGE_ID = nil
34
+ DEFAULT_CORRELATION_ID = nil
35
+ DEFAULT_REPLY_TO = nil
36
+ DEFAULT_TYPE = nil
37
+ DEFAULT_USER_ID = nil
38
+ DEFAULT_APP_ID = nil
39
+ DEFAULT_TIMESTAMP = nil
40
+ DEFAULT_EXPIRATION = nil
41
+
42
+ def defaults
43
+ @defaults ||= Hash[Config.constants.select { |c| c.to_s.start_with? 'DEFAULT_' }.collect do |c|
44
+ key = c.slice(8..-1).downcase.to_sym
45
+
46
+ env_key = "#{ENV_PREFIX}_#{key}".upcase
47
+
48
+ [key, ENV[env_key] || Config.const_get(c)]
49
+ end
50
+ ]
51
+ end
52
+
53
+ Config.constants.each do |c|
54
+ if c.to_s.start_with? 'DEFAULT_'
55
+ attr_symbol = c.slice(8..-1).downcase.to_sym
56
+ attr_writer attr_symbol
57
+ class_eval %Q{
58
+ def #{attr_symbol}
59
+ @#{attr_symbol} ||= defaults[:#{attr_symbol}]
60
+ end
61
+ }
62
+ end
63
+ end
64
+
65
+ def load(config)
66
+ Philotic.logger # ensure the logger can be created, so we crash early if it can't
67
+
68
+ config.each do |k, v|
69
+ mutator = "#{k}="
70
+ send(mutator, v) if respond_to? mutator
71
+ end
72
+ end
73
+
74
+ def load_file(filename, env = 'development')
75
+ config = YAML.load_file(filename)
76
+ load(config[env])
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,83 @@
1
+ require 'singleton'
2
+ require 'json'
3
+ require 'amqp'
4
+ require "amqp/extensions/rabbitmq"
5
+ require 'amqp/utilities/event_loop_helper'
6
+
7
+ module Philotic
8
+ module Connection
9
+ extend self
10
+ attr_reader :connection
11
+
12
+ def config
13
+ Philotic::Config
14
+ end
15
+
16
+ def connect! &block
17
+ if connected?
18
+ Philotic.logger.info "already connected to RabbitMQ; host:#{config.rabbit_host}"
19
+ block.call if block
20
+ return
21
+ end
22
+
23
+ AMQP::Utilities::EventLoopHelper.run do
24
+ connection_settings = {
25
+ host: config.rabbit_host,
26
+ port: config.rabbit_port,
27
+ user: config.rabbit_user,
28
+ password: config.rabbit_password,
29
+ vhost: config.rabbit_vhost,
30
+ timeout: config.timeout,
31
+ on_tcp_connection_failure: config.connection_failed_handler,
32
+ }
33
+
34
+ AMQP.start(connection_settings, logging: true) do |connection, connect_ok|
35
+ @connection = connection
36
+
37
+ if connected?
38
+ Philotic.logger.info "connected to RabbitMQ; host:#{config.rabbit_host}"
39
+ else
40
+ Philotic.logger.warn "failed connected to RabbitMQ; host:#{config.rabbit_host}"
41
+ end
42
+
43
+
44
+ AMQP.channel.auto_recovery = true
45
+
46
+ @connection.on_tcp_connection_loss do |cl, settings|
47
+ config.method(:connection_loss_handler).call.call(cl, settings)
48
+ end
49
+
50
+ @connection.after_recovery do |conn, settings|
51
+ Philotic.logger.info "Connection recovered, now connected to #{config.rabbit_host}"
52
+ end
53
+
54
+ setup_exchange_handler!
55
+ block.call if block
56
+
57
+ end #AMQP.start
58
+ end
59
+ end
60
+
61
+ def close &block
62
+ if connected?
63
+ connection.close &block
64
+ else
65
+ block.call
66
+ end
67
+ end
68
+
69
+ def connected?
70
+ connection && connection.connected?
71
+ end
72
+
73
+ def exchange
74
+ AMQP.channel.headers(config.exchange_name, durable: true)
75
+ end
76
+
77
+ def setup_exchange_handler!
78
+ exchange.on_return do |basic_return, metadata, payload|
79
+ config.method(:message_return_handler).call.call(basic_return, metadata, payload)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,8 @@
1
+ module Philotic
2
+ class DummyEvent < Event
3
+ attr_payload :subject
4
+ attr_payload :message
5
+ attr_routable :gender
6
+ attr_routable :available
7
+ end
8
+ end
@@ -0,0 +1,69 @@
1
+ require 'philotic/connection'
2
+
3
+ module Philotic
4
+ class Event
5
+ include Philotic::Routable
6
+
7
+ def self.inherited(sub)
8
+ Philotic::PHILOTIC_HEADERS.each do |header|
9
+ sub.attr_routable header
10
+ end
11
+ self.attr_routable_readers.dup.each do |routable|
12
+ sub.attr_routable routable
13
+ end
14
+
15
+ self.attr_payload_readers.dup.each do |payload|
16
+ sub.attr_payload payload
17
+ end
18
+ end
19
+
20
+ self.inherited(self)
21
+
22
+ Philotic::MESSAGE_OPTIONS.each do |message_option|
23
+ attr_reader message_option
24
+ define_method :"#{message_option}=" do |val|
25
+ instance_variable_set(:"@#{message_option}", val)
26
+ self.message_metadata[message_option] = val
27
+ end
28
+ end
29
+
30
+ def connection
31
+ Philotic::Connection.instance
32
+ end
33
+
34
+ def set_routables_or_payloads(type, attrs)
35
+ attrs.each do |key, value|
36
+ if self.respond_to?(:"#{key}=")
37
+ send(:"#{key}=", value)
38
+ elsif self.class == Philotic::Event
39
+ self.class.send("attr_#{type}_readers").concat([key])
40
+ self.class.send("attr_#{type}_writers").concat([:"#{key}="])
41
+
42
+ setter = Proc.new do |v|
43
+ instance_variable_set(:"@#{key}", v)
44
+ end
45
+ getter = Proc.new do
46
+ instance_variable_get(:"@#{key}")
47
+ end
48
+ self.class.send :define_method, :"#{key}=", setter
49
+ self.send(:"#{key}=", value)
50
+ self.class.send :define_method, :"#{key}", getter
51
+ end
52
+ end
53
+ end
54
+
55
+ def initialize(routables={}, payloads=nil)
56
+ payloads ||= {}
57
+ self.timestamp = Time.now.to_i
58
+ self.philotic_firehose = true
59
+
60
+ # dynamically insert any passed in routables into both attr_routable
61
+ # and attr_payload
62
+ # result: ability to arbitrarily send a easily routable hash
63
+ # over into the bus
64
+ set_routables_or_payloads(:routable, routables)
65
+ set_routables_or_payloads(:payload, payloads)
66
+ self
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,58 @@
1
+ require 'philotic/connection'
2
+ module Philotic
3
+ module Publisher
4
+ extend self
5
+
6
+ def config
7
+ Philotic::Config
8
+ end
9
+
10
+ def publish(event, &block)
11
+ message_metadata = {headers: event.headers}
12
+ message_metadata.merge!(event.message_metadata) if event.message_metadata
13
+ raw_publish(event.payload, message_metadata, &block)
14
+ end
15
+
16
+ def raw_publish(payload, message_metadata = {}, &block)
17
+
18
+ if Philotic.connected?
19
+ _raw_publish payload, message_metadata, &block
20
+ else
21
+ Philotic.connect! do
22
+ _raw_publish payload, message_metadata, &block
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+ def _raw_publish(payload, message_metadata = {}, &block)
29
+
30
+ publish_defaults = {}
31
+ Philotic::MESSAGE_OPTIONS.each do |key|
32
+ publish_defaults[key] = config.send(key.to_s)
33
+ end
34
+ message_metadata = publish_defaults.merge message_metadata
35
+ message_metadata[:headers] ||= {}
36
+ message_metadata[:headers] = {philotic_firehose: true}.merge(message_metadata[:headers])
37
+
38
+
39
+ payload.each { |k, v| payload[k] = v.utc if v.is_a? ActiveSupport::TimeWithZone }
40
+
41
+ callback = Proc.new do
42
+ Philotic.log_event_published(:debug, message_metadata, payload, 'published event')
43
+ block.call if block
44
+ end
45
+
46
+ if config.disable_publish
47
+ EventMachine.next_tick(&callback)
48
+ return
49
+ end
50
+
51
+ unless Philotic::Connection.connected?
52
+ Philotic.log_event_published(:error, message_metadata, payload, 'unable to publish event, not connected to amqp broker')
53
+ return
54
+ end
55
+ Thread.new { Philotic::Connection.exchange.publish(payload.to_json, message_metadata, &callback) }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,106 @@
1
+ require 'active_support/all'
2
+ require 'active_record'
3
+
4
+ module Philotic
5
+ module Routable
6
+ def self.included(base)
7
+ base.send :include, ActiveRecord::Validations
8
+ base.send :include, ActiveRecord::Callbacks
9
+ base.validates :philotic_firehose, :philotic_product, :philotic_component, :philotic_event_type, presence: true
10
+
11
+ base.extend ClassMethods
12
+ end
13
+
14
+ def payload
15
+ attribute_hash = {}
16
+ self.class.attr_payload_readers.each do |attr|
17
+ attr = attr.to_sym
18
+ attribute_hash[attr] = send(attr)
19
+ end
20
+ attribute_hash
21
+ end
22
+
23
+ def headers
24
+ attribute_hash = {}
25
+ self.class.attr_routable_readers.each do |attr|
26
+ attr = attr.to_sym
27
+ attribute_hash[attr] = send(attr)
28
+ end
29
+ attribute_hash
30
+ end
31
+
32
+ def attributes
33
+ attribute_hash = {}
34
+ (self.class.attr_payload_readers + self.class.attr_routable_readers).each do |attr|
35
+ attr = attr.to_sym
36
+ attribute_hash[attr] = send(attr)
37
+ end
38
+ attribute_hash
39
+ end
40
+
41
+ def message_metadata
42
+ @message_metadata ||= {}
43
+ end
44
+
45
+ def message_metadata= options
46
+ @message_metadata ||= {}
47
+ @message_metadata.merge! options
48
+ end
49
+
50
+ def publish &block
51
+ Philotic::Publisher.publish(self, &block)
52
+ end
53
+
54
+ module ClassMethods
55
+ def attr_payload_reader *names
56
+ attr_payload_readers.concat(names)
57
+ attr_reader(*names)
58
+ end
59
+
60
+ def attr_payload_readers
61
+ @attr_payload_readers ||= []
62
+ end
63
+
64
+ def attr_payload_writer *names
65
+ attr_payload_writers.concat names
66
+ attr_writer(*names)
67
+ end
68
+
69
+ def attr_payload_writers
70
+ @attr_payload_writers ||= []
71
+ end
72
+
73
+ def attr_payload *names
74
+ names -= attr_payload_readers
75
+ attr_payload_readers.concat(names)
76
+ attr_payload_writers.concat(names)
77
+ attr_accessor(*names)
78
+ end
79
+
80
+ def attr_routable_reader *names
81
+ attr_routable_reader.concat(names)
82
+ attr_reader(*names)
83
+ end
84
+
85
+ def attr_routable_readers
86
+ @attr_routable_readers ||= []
87
+ end
88
+
89
+ def attr_routable_writers
90
+ @attr_routable_writers ||= []
91
+ end
92
+
93
+ def attr_routable_writer *names
94
+ attr_routable_writers.concat names
95
+ attr_writer(*names)
96
+ end
97
+
98
+ def attr_routable *names
99
+ names -= attr_routable_readers
100
+ attr_routable_readers.concat(names)
101
+ attr_routable_writers.concat(names)
102
+ attr_accessor(*names)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,67 @@
1
+ module Philotic
2
+ class Subscriber
3
+ def self.subscribe(options = {}, subscribe_options = Philotic::DEFAULT_SUBSCRIBE_OPTIONS, &block)
4
+ if Philotic.connected?
5
+ _subscribe(options, subscribe_options, &block)
6
+ else
7
+ Philotic.connect! do
8
+ _subscribe(options, subscribe_options, &block)
9
+ end
10
+ end
11
+ end
12
+
13
+ def self.subscribe_to_any_of(options = {}, &block)
14
+ arguments = options[:arguments] || {}
15
+ queue_options = options[:queue_options] || {}
16
+
17
+ arguments['x-match'] = 'any'
18
+
19
+ self.subscribe(options, &block)
20
+ end
21
+
22
+ def self.subscribe_to_all_of(options = {}, &block)
23
+ arguments = options[:arguments] || {}
24
+ queue_options = options[:queue_options] || {}
25
+
26
+ arguments['x-match'] = 'all'
27
+
28
+ self.subscribe(options, &block)
29
+ end
30
+
31
+ private
32
+ def self._subscribe(options = {}, subscribe_options = Philotic::DEFAULT_SUBSCRIBE_OPTIONS, &block)
33
+ @@exchange = Philotic::Connection.exchange
34
+
35
+ if options.is_a? String
36
+ queue_name = options
37
+ queue_options = Philotic::DEFAULT_NAMED_QUEUE_OPTIONS
38
+ else
39
+ queue_name = options[:queue_name] || ""
40
+
41
+ queue_options = Philotic::DEFAULT_ANONYMOUS_QUEUE_OPTIONS.merge(options[:queue_options] || {})
42
+ subscribe_options = subscribe_options.merge(options[:subscribe_options]) if options[:subscribe_options]
43
+ arguments = options[:arguments] || options
44
+ arguments['x-match'] ||= 'all'
45
+ end
46
+
47
+ queue_options[:auto_delete] ||= true if queue_name == ""
48
+
49
+ callback = Proc.new do |metadata, payload|
50
+ hash_payload = JSON.parse payload
51
+
52
+ event = {
53
+ payload: hash_payload,
54
+ headers: metadata.attributes[:headers],
55
+ attributes: metadata.attributes[:headers] ? hash_payload.merge(metadata.attributes[:headers]) : hash_payload
56
+ }
57
+ block.call(metadata, event)
58
+ end
59
+ q = AMQP.channel.queue(queue_name, queue_options)
60
+
61
+ q.bind(@@exchange, arguments: arguments) if arguments
62
+
63
+ q.subscribe(subscribe_options, &callback)
64
+
65
+ end
66
+ end
67
+ end