philotic 0.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.
@@ -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