philotic 0.0.1 → 0.1.0

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -1
  3. data/Gemfile +0 -7
  4. data/README.md +2 -0
  5. data/Rakefile +1 -1
  6. data/examples/README.md +1 -1
  7. data/examples/creating_named_queues/manually.rb +8 -21
  8. data/examples/creating_named_queues/with_rake.rb +2 -2
  9. data/examples/publishing/publish.rb +26 -33
  10. data/examples/subscribing/acks.rb +25 -0
  11. data/examples/subscribing/anonymous_queue.rb +6 -10
  12. data/examples/subscribing/multiple_named_queues.rb +9 -24
  13. data/examples/subscribing/named_queue.rb +7 -17
  14. data/lib/philotic.rb +37 -81
  15. data/lib/philotic/config.rb +63 -24
  16. data/lib/philotic/connection.rb +35 -51
  17. data/lib/philotic/constants.rb +49 -0
  18. data/lib/philotic/event.rb +27 -14
  19. data/lib/philotic/logging.rb +8 -0
  20. data/lib/philotic/logging/event.rb +18 -0
  21. data/lib/philotic/logging/logger.rb +39 -0
  22. data/lib/philotic/publisher.rb +28 -31
  23. data/lib/philotic/routable.rb +40 -40
  24. data/lib/philotic/subscriber.rb +61 -42
  25. data/lib/philotic/tasks/init_queues.rb +4 -14
  26. data/lib/philotic/version.rb +1 -1
  27. data/philotic.gemspec +21 -10
  28. data/spec/philotic/config_spec.rb +40 -0
  29. data/spec/philotic/connection_spec.rb +58 -0
  30. data/spec/philotic/event_spec.rb +69 -0
  31. data/spec/philotic/logging/logger_spec.rb +26 -0
  32. data/spec/philotic/publisher_spec.rb +99 -0
  33. data/spec/{routable_spec.rb → philotic/routable_spec.rb} +15 -14
  34. data/spec/philotic/subscriber_spec.rb +111 -0
  35. data/spec/philotic_spec.rb +66 -0
  36. data/spec/spec_helper.rb +12 -4
  37. data/tasks/bump.rake +10 -10
  38. metadata +173 -36
  39. data/spec/connection_spec.rb +0 -19
  40. data/spec/event_spec.rb +0 -44
  41. data/spec/publisher_spec.rb +0 -102
  42. data/spec/subscriber_spec.rb +0 -72
@@ -2,6 +2,8 @@ require 'yaml'
2
2
  require 'json'
3
3
  require 'singleton'
4
4
  require 'forwardable'
5
+ require 'cgi'
6
+ require 'bunny/session'
5
7
 
6
8
  module Philotic
7
9
  module Config
@@ -11,33 +13,29 @@ module Philotic
11
13
 
12
14
  DEFAULT_DISABLE_PUBLISH = false
13
15
 
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}"; }
16
+ DEFAULT_RABBIT_HOST = 'localhost'
17
+ DEFAULT_RABBIT_PORT = 5672
18
+ DEFAULT_RABBIT_USER = 'guest'
19
+ DEFAULT_RABBIT_PASSWORD = 'guest'
20
+ DEFAULT_RABBIT_VHOST = '%2f' # '/'
21
+ DEFAULT_RABBIT_URL = "amqp://#{DEFAULT_RABBIT_USER}:#{DEFAULT_RABBIT_PASSWORD}@#{DEFAULT_RABBIT_HOST}:#{DEFAULT_RABBIT_PORT}/#{DEFAULT_RABBIT_VHOST}"
22
+ DEFAULT_EXCHANGE_NAME = 'philotic.headers'
24
23
  DEFAULT_TIMEOUT = 2
25
-
26
- DEFAULT_ROUTING_KEY = nil
27
- DEFAULT_PERSISTENT = true
24
+ DEFAULT_ROUTING_KEY = nil
25
+ DEFAULT_PERSISTENT = true
28
26
  # DEFAULT_IMMEDIATE = false
29
- DEFAULT_MANDATORY = true
30
- DEFAULT_CONTENT_TYPE = nil
27
+ DEFAULT_MANDATORY = true
28
+ DEFAULT_CONTENT_TYPE = nil
31
29
  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
30
+ DEFAULT_PRIORITY = nil
31
+ DEFAULT_MESSAGE_ID = nil
32
+ DEFAULT_CORRELATION_ID = nil
33
+ DEFAULT_REPLY_TO = nil
34
+ DEFAULT_TYPE = nil
35
+ DEFAULT_USER_ID = nil
36
+ DEFAULT_APP_ID = nil
37
+ DEFAULT_TIMESTAMP = nil
38
+ DEFAULT_EXPIRATION = nil
41
39
 
42
40
  def defaults
43
41
  @defaults ||= Hash[Config.constants.select { |c| c.to_s.start_with? 'DEFAULT_' }.collect do |c|
@@ -62,6 +60,47 @@ module Philotic
62
60
  end
63
61
  end
64
62
 
63
+ attr_reader :connection_failed_handler, :connection_loss_handler, :message_return_handler
64
+
65
+ def connection_failed_handler
66
+ @connection_failed_handler ||= lambda do |settings|
67
+ Philotic.logger.error "RabbitMQ connection failure; host:#{rabbit_host}"
68
+ end
69
+ end
70
+
71
+ def connection_loss_handler
72
+ @connection_loss_handler ||= lambda do |conn, settings|
73
+ Philotic.logger.warn "RabbitMQ connection loss; host:#{rabbit_host}"; conn.reconnect(false, 2)
74
+ end
75
+ end
76
+
77
+ def message_return_handler
78
+ @message_return_handler ||= lambda do |basic_return, metadata, payload|
79
+ puts "Philotic message #{JSON.parse payload} was returned! reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text} headers = #{metadata[:headers]}"
80
+ end
81
+ end
82
+
83
+ def self.parse_rabbit_uri
84
+ settings = Bunny::Session.parse_uri(@rabbit_url || defaults[:rabbit_url])
85
+ settings[:password] = settings.delete(:pass)
86
+
87
+ %w[host port user password vhost].each do |setting|
88
+ setting = setting.to_sym
89
+ current_value = send("rabbit_#{setting}")
90
+
91
+ # only use the value from the URI if the existing value is nil or the default
92
+ if settings[setting] && [const_get("default_rabbit_#{setting}".upcase), nil].include?(current_value)
93
+ send("rabbit_#{setting}=", settings[setting])
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ def self.rabbit_url
100
+ self.parse_rabbit_uri
101
+ "amqp://#{rabbit_user}:#{rabbit_password}@#{rabbit_host}:#{rabbit_port}/#{CGI.escape rabbit_vhost}"
102
+ end
103
+
65
104
  def load(config)
66
105
  Philotic.logger # ensure the logger can be created, so we crash early if it can't
67
106
 
@@ -1,8 +1,8 @@
1
1
  require 'singleton'
2
2
  require 'json'
3
- require 'amqp'
4
- require "amqp/extensions/rabbitmq"
5
- require 'amqp/utilities/event_loop_helper'
3
+ require 'bunny'
4
+
5
+ require 'philotic/config'
6
6
 
7
7
  module Philotic
8
8
  module Connection
@@ -13,70 +13,54 @@ module Philotic
13
13
  Philotic::Config
14
14
  end
15
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
16
+ def connect!
17
+ return if connected?
36
18
 
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
19
+ start_connection!
42
20
 
21
+ if connected?
22
+ Philotic.logger.info "connected to RabbitMQ: #{config.rabbit_host}:#{config.rabbit_port}"
23
+ set_exchange_return_handler!
24
+ true
25
+ else
26
+ Philotic.logger.error "failed connected to RabbitMQ; host:#{config.rabbit_host}"
27
+ false
28
+ end
29
+ end
43
30
 
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
31
+ def start_connection!
32
+ @connection = Bunny.new(config.rabbit_url, connection_settings)
33
+ @connection.start
34
+ end
56
35
 
57
- end #AMQP.start
58
- end
36
+ def connection_settings
37
+ {
38
+ timeout: config.timeout.to_i,
39
+ automatically_recover: true,
40
+ on_tcp_connection_failure: config.connection_failed_handler,
41
+ }
59
42
  end
60
43
 
61
- def close &block
62
- if connected?
63
- connection.close &block
64
- else
65
- block.call
66
- end
44
+ def close
45
+ Philotic.logger.info "closing connection to RabbitMQ; host:#{config.rabbit_host}"
46
+ connection.close if connected?
67
47
  end
68
48
 
69
49
  def connected?
70
50
  connection && connection.connected?
71
51
  end
72
52
 
53
+ def channel
54
+ @channel ||= connection.create_channel
55
+ end
56
+
73
57
  def exchange
74
- AMQP.channel.headers(config.exchange_name, durable: true)
58
+ @exchange ||= channel.headers(config.exchange_name, durable: true)
75
59
  end
76
60
 
77
- def setup_exchange_handler!
61
+ def set_exchange_return_handler!
78
62
  exchange.on_return do |basic_return, metadata, payload|
79
- config.method(:message_return_handler).call.call(basic_return, metadata, payload)
63
+ config.message_return_handler.call(basic_return, metadata, payload)
80
64
  end
81
65
  end
82
66
  end
@@ -0,0 +1,49 @@
1
+ module Philotic
2
+
3
+ CONNECTION_OPTIONS = [
4
+ :rabbit_host,
5
+ :connection_failed_handler,
6
+ :connection_loss_handler,
7
+ :timeout,
8
+ ]
9
+ EXCHANGE_OPTIONS = [
10
+ :exchange_name,
11
+ :message_return_handler,
12
+ ]
13
+ MESSAGE_OPTIONS = [
14
+ :routing_key,
15
+ :persistent,
16
+ # :immediate,
17
+ :mandatory,
18
+ :content_type,
19
+ :content_encoding,
20
+ :priority,
21
+ :message_id,
22
+ :correlation_id,
23
+ :reply_to,
24
+ :type,
25
+ :user_id,
26
+ :app_id,
27
+ :timestamp,
28
+ :expiration,
29
+ ]
30
+
31
+ PHILOTIC_HEADERS = [
32
+ :philotic_firehose,
33
+ :philotic_product,
34
+ :philotic_component,
35
+ :philotic_event_type,
36
+ ]
37
+
38
+ DEFAULT_NAMED_QUEUE_OPTIONS = {
39
+ auto_delete: false,
40
+ durable: true
41
+ }
42
+ DEFAULT_ANONYMOUS_QUEUE_OPTIONS = {
43
+ auto_delete: true,
44
+ durable: false
45
+ }
46
+
47
+ DEFAULT_SUBSCRIBE_OPTIONS = {}
48
+
49
+ end
@@ -1,4 +1,6 @@
1
+ require 'philotic/constants'
1
2
  require 'philotic/connection'
3
+ require 'philotic/routable'
2
4
 
3
5
  module Philotic
4
6
  class Event
@@ -36,25 +38,36 @@ module Philotic
36
38
  if self.respond_to?(:"#{key}=")
37
39
  send(:"#{key}=", value)
38
40
  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
41
+ set_event_attribute(type, key, value)
51
42
  end
52
43
  end
53
44
  end
54
45
 
46
+ def set_event_attribute(type, key, value)
47
+ set_event_attribute_setter(key, type, value)
48
+ set_event_attribute_getter(key, type)
49
+ end
50
+
51
+ def set_event_attribute_getter(key, type)
52
+ self.class.send("attr_#{type}_readers").concat([key])
53
+ getter = lambda do
54
+ instance_variable_get(:"@#{key}")
55
+ end
56
+ self.class.send :define_method, :"#{key}", getter
57
+ end
58
+
59
+ def set_event_attribute_setter(key, type, value)
60
+ self.class.send("attr_#{type}_writers").concat([:"#{key}="])
61
+ setter = lambda do |v|
62
+ instance_variable_set(:"@#{key}", v)
63
+ end
64
+ self.class.send :define_method, :"#{key}=", setter
65
+ self.send(:"#{key}=", value)
66
+ end
67
+
55
68
  def initialize(routables={}, payloads=nil)
56
- payloads ||= {}
57
- self.timestamp = Time.now.to_i
69
+ payloads ||= {}
70
+ self.timestamp = Time.now.to_i
58
71
  self.philotic_firehose = true
59
72
 
60
73
  # dynamically insert any passed in routables into both attr_routable
@@ -0,0 +1,8 @@
1
+ require 'philotic/logging/event'
2
+ require 'philotic/logging/logger'
3
+
4
+ module Philotic
5
+ module Logging
6
+
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ require 'philotic/event'
2
+
3
+ module Philotic
4
+ module Logging
5
+ class Event < Philotic::Event
6
+ attr_routable :severity, :progname
7
+ attr_payload :message
8
+
9
+ def initialize(severity, message = nil, progname = nil)
10
+ super({})
11
+ self.severity = severity
12
+ self.message = message
13
+ self.progname = progname
14
+
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ require 'logger'
2
+ require 'philotic/logging/event'
3
+
4
+ module Philotic
5
+ module Logging
6
+ class Logger < ::Logger
7
+
8
+ attr_writer :event_class
9
+
10
+ def event_class
11
+ @event_class ||= Philotic::Logging::Event
12
+ end
13
+
14
+ def add(severity, message = nil, progname = nil, &block)
15
+ severity ||= UNKNOWN
16
+ if @logdev.nil? or severity < @level
17
+ return true
18
+ end
19
+ progname ||= @progname
20
+ if message.nil?
21
+ if block_given?
22
+ message = yield
23
+ else
24
+ message = progname
25
+ progname = @progname
26
+ end
27
+ end
28
+ @logdev.write(
29
+ format_message(format_severity(severity), Time.now, progname, message))
30
+ event_class.publish(severity, message, progname)
31
+
32
+ true
33
+ end
34
+
35
+ alias log add
36
+
37
+ end
38
+ end
39
+ end
@@ -1,4 +1,5 @@
1
1
  require 'philotic/connection'
2
+
2
3
  module Philotic
3
4
  module Publisher
4
5
  extend self
@@ -7,52 +8,48 @@ module Philotic
7
8
  Philotic::Config
8
9
  end
9
10
 
10
- def publish(event, &block)
11
+ def publish(event)
11
12
  message_metadata = {headers: event.headers}
12
13
  message_metadata.merge!(event.message_metadata) if event.message_metadata
13
- raw_publish(event.payload, message_metadata, &block)
14
+ _publish(event.payload, message_metadata)
14
15
  end
15
16
 
16
- def raw_publish(payload, message_metadata = {}, &block)
17
+ private
18
+ def _publish(payload, message_metadata = {})
19
+ Philotic.connect!
20
+ unless Philotic::Connection.connected?
21
+ Philotic.log_event_published(:error, message_metadata, payload, 'unable to publish event, not connected to RabbitMQ')
22
+ return
23
+ end
24
+ message_metadata = merge_metadata(message_metadata)
25
+
26
+ payload = normalize_payload_times(payload)
17
27
 
18
- if Philotic.connected?
19
- _raw_publish payload, message_metadata, &block
20
- else
21
- Philotic.connect! do
22
- _raw_publish payload, message_metadata, &block
28
+ return if config.disable_publish
29
+
30
+ Philotic::Connection.exchange.publish(payload.to_json, message_metadata)
31
+ Philotic.log_event_published(:debug, message_metadata, payload, 'published event')
32
+ end
33
+
34
+ def normalize_payload_times(payload)
35
+ payload.each do |k, v|
36
+ if v.respond_to?(:utc)
37
+ payload[k] = v.utc
38
+ elsif v.respond_to?(:to_utc)
39
+ payload[k] = v.to_utc
23
40
  end
24
41
  end
25
42
  end
26
43
 
27
- private
28
- def _raw_publish(payload, message_metadata = {}, &block)
29
-
44
+ def merge_metadata(message_metadata)
30
45
  publish_defaults = {}
31
46
  Philotic::MESSAGE_OPTIONS.each do |key|
32
47
  publish_defaults[key] = config.send(key.to_s)
33
48
  end
34
- message_metadata = publish_defaults.merge message_metadata
49
+ message_metadata = publish_defaults.merge message_metadata
35
50
  message_metadata[:headers] ||= {}
36
51
  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) }
52
+ message_metadata
56
53
  end
57
54
  end
58
55
  end