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.
- checksums.yaml +7 -0
- data/.gitignore +25 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/Gemfile +11 -0
- data/LICENSE +22 -0
- data/README.md +7 -0
- data/Rakefile +24 -0
- data/examples/README.md +13 -0
- data/examples/creating_named_queues/manually.rb +26 -0
- data/examples/creating_named_queues/philotic_named_queues.yml +6 -0
- data/examples/creating_named_queues/with_rake.rb +11 -0
- data/examples/publishing/publish.rb +51 -0
- data/examples/subscribing/anonymous_queue.rb +19 -0
- data/examples/subscribing/multiple_named_queues.rb +33 -0
- data/examples/subscribing/named_queue.rb +23 -0
- data/lib/philotic.rb +144 -0
- data/lib/philotic/config.rb +79 -0
- data/lib/philotic/connection.rb +83 -0
- data/lib/philotic/dummy_event.rb +8 -0
- data/lib/philotic/event.rb +69 -0
- data/lib/philotic/publisher.rb +58 -0
- data/lib/philotic/routable.rb +106 -0
- data/lib/philotic/subscriber.rb +67 -0
- data/lib/philotic/tasks.rb +1 -0
- data/lib/philotic/tasks/init_queues.rb +28 -0
- data/lib/philotic/version.rb +3 -0
- data/philotic.gemspec +24 -0
- data/philotic.yml.example +36 -0
- data/philotic_queues.yml.example +19 -0
- data/spec/connection_spec.rb +19 -0
- data/spec/event_spec.rb +44 -0
- data/spec/publisher_spec.rb +102 -0
- data/spec/routable_spec.rb +58 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/subscriber_spec.rb +72 -0
- data/tasks/bump.rake +30 -0
- metadata +157 -0
@@ -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,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
|