philotic 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|