active_publisher 0.1.5-java → 0.2.0.pre-java

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 658bc2636e515d48b346bde7f92421e7c9dfe9dd
4
- data.tar.gz: 8b454665dbc31565a67184d4b699463f685049be
3
+ metadata.gz: 63df17dc2a58125dcc18b1cb9c7d20f0517da0ac
4
+ data.tar.gz: 9bd8dcd05da48bde7c14ff49a3dc10928ab609a6
5
5
  SHA512:
6
- metadata.gz: 3e3ef0abdc6abc0c4bc3995966c8197f5c803581ea549c109762dff9b40b7fc59ce39c12b00079d0b596e5b6b90ab298cfbc53a6b116906409d33940cb1e6e72
7
- data.tar.gz: 096f63f02a40ace574301c542d657a85f0a23c151ec058ae9ca9f3179b845b01e0d350aae53d41705225f4c06307f74b114e298da552e18433a2c7480a3d8a3e
6
+ metadata.gz: e8dbdc550e66d0d4182ed96333a03e37ab89466371bc3bc4634ff6d78e1f02e8daeafb5ed3b13f0bc3cb91d793af46ee6954ba2519e148122d8b2dde569a025c
7
+ data.tar.gz: d6a7965709096d9f9828afe0655982aaa5ddafa87b3256e44d6c815df4b6467af64bd0236d5f3c49edb9dd366f28dbb2e3011c1373ebd86a8ea9928bf8418bb6
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
+ .ruby-*
1
2
  /.bundle/
2
3
  /.yardoc
3
4
  /Gemfile.lock
data/README.md CHANGED
@@ -20,9 +20,76 @@ Or install it yourself as:
20
20
 
21
21
  $ gem install active_publisher
22
22
 
23
+ ## Configuration
24
+
25
+ ActivePublisher will use a `config/active_publisher.yml` or `config/action_subscriber.yml` automatically.
26
+
27
+ Create a `config/active_publisher.yml` similar to a database.yml, with your configuration nested in your environments keys.
28
+
29
+ ```yaml
30
+ default: &default
31
+ host: localhost
32
+ username: guest
33
+ password: guest
34
+
35
+ development:
36
+ <<: *default
37
+
38
+ test:
39
+ <<: *default
40
+
41
+ production:
42
+ <<: *default
43
+ host: <%= ENV['RABBIT_MQ_HOST'] %>
44
+ username: <%= ENV['RABBIT_MQ_USERNAME'] %>
45
+ password: <%= ENV['RABBIT_MQ_PASSWORD'] %>
46
+ ```
47
+
48
+ Defaults for the configuration are:
49
+ ```ruby
50
+ {
51
+ :heartbeat => 5,
52
+ :host => "localhost",
53
+ :hosts => [],
54
+ :port => 5672,
55
+ :publisher_confirms => false,
56
+ :seconds_to_wait_for_graceful_shutdown => 30,
57
+ :timeout => 1,
58
+ :username => "guest",
59
+ :password => "guest",
60
+ :virtual_host => "/"
61
+ }
62
+ ```
63
+
23
64
  ## Usage
24
65
 
25
- TODO: Write usage instructions here
66
+ Basic publishing is simple.
67
+
68
+ ```ruby
69
+ # @param [String] route The routing key to use for this message.
70
+ # @param [String] payload The message you are sending. Should already be encoded as a string.
71
+ # @param [String] exchange The exchange you want to publish to.
72
+ # @param [Hash] options hash to set message parameters (e.g. headers)
73
+
74
+ ::ActivePublisher.publish("user.created", user.to_json, "events", {})
75
+ ```
76
+
77
+
78
+ Async publishing is as simple as configuring the async publishing adapter and running `publish_sync` the same was as publish.
79
+ You can use the `::ActivePublisher::Async::InMemoryAdapter` that ships with `ActivePublisher`.
80
+
81
+
82
+ `initializers/active_publisher.rb`
83
+ ```ruby
84
+ require "active_publisher"
85
+ ::ActivePublisher::Async.publisher_adapter = ::ActivePublisher::Async::InMemoryAdapter.new
86
+
87
+ ```
88
+
89
+ ```ruby
90
+ ::ActivePublisher.publish_async("user.created", user.to_json, "events", {})
91
+ ```
92
+
26
93
 
27
94
  ## Development
28
95
 
@@ -26,6 +26,8 @@ Gem::Specification.new do |spec|
26
26
  spec.add_dependency 'bunny', '~> 2.1'
27
27
  end
28
28
 
29
+ spec.add_dependency 'multi_op_queue', '>= 0.1.2'
30
+
29
31
  spec.add_development_dependency "bundler"
30
32
  spec.add_development_dependency "pry"
31
33
  spec.add_development_dependency "rake", "~> 10.0"
@@ -0,0 +1,61 @@
1
+ module ActivePublisher
2
+ module Async
3
+ module InMemoryAdapter
4
+ class AsyncQueue
5
+ include ::ActivePublisher::Logging
6
+
7
+ attr_accessor :drop_messages_when_queue_full,
8
+ :max_queue_size,
9
+ :supervisor_interval
10
+
11
+ attr_reader :consumer, :queue, :supervisor
12
+
13
+ def initialize(drop_messages_when_queue_full, max_queue_size, supervisor_interval)
14
+ @drop_messages_when_queue_full = drop_messages_when_queue_full
15
+ @max_queue_size = max_queue_size
16
+ @supervisor_interval = supervisor_interval
17
+ @queue = ::MultiOpQueue::Queue.new
18
+ create_and_supervise_consumer!
19
+ end
20
+
21
+ def push(message)
22
+ # default of 1_000_000 messages
23
+ if queue.size > max_queue_size
24
+ # Drop messages if the queue is full and we were configured to do so
25
+ return if drop_messages_when_queue_full
26
+
27
+ # By default we will raise an error to push the responsibility onto the caller
28
+ fail ::ActivePublisher::Async::InMemoryAdapter::UnableToPersistMessageError, "Queue is full, messages will be dropped."
29
+ end
30
+
31
+ queue.push(message)
32
+ end
33
+
34
+ def size
35
+ queue.size
36
+ end
37
+
38
+ private
39
+
40
+ def create_and_supervise_consumer!
41
+ @consumer = ::ActivePublisher::Async::InMemoryAdapter::ConsumerThread.new(queue)
42
+ @supervisor = ::Thread.new do
43
+ loop do
44
+ unless consumer.alive?
45
+ # We might need to requeue the last messages popped
46
+ current_consumer_messages = consumer.current_messages
47
+ queue.concat(current_consumer_messages) unless current_consumer_messages.empty?
48
+ consumer.kill
49
+ @consumer = ::ActivePublisher::Async::InMemoryAdapter::ConsumerThread.new(queue)
50
+ end
51
+
52
+ # Pause before checking the consumer again.
53
+ sleep supervisor_interval
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,89 @@
1
+ module ActivePublisher
2
+ module Async
3
+ module InMemoryAdapter
4
+ class ConsumerThread
5
+ attr_reader :current_messages, :thread, :queue
6
+
7
+ if ::RUBY_PLATFORM == "java"
8
+ NETWORK_ERRORS = [::MarchHare::Exception, ::Java::ComRabbitmqClient::AlreadyClosedException, ::Java::JavaIo::IOException].freeze
9
+ else
10
+ NETWORK_ERRORS = [::Bunny::Exception, ::Timeout::Error, ::IOError].freeze
11
+ end
12
+
13
+ def initialize(listen_queue)
14
+ @current_messages = []
15
+ @queue = listen_queue
16
+ start_thread
17
+ end
18
+
19
+ def alive?
20
+ @thread && @thread.alive?
21
+ end
22
+
23
+ def kill
24
+ @thread.kill if @thread
25
+ @thread = nil
26
+ end
27
+
28
+ private
29
+
30
+ def await_network_reconnect
31
+ if defined?(ActivePublisher::RabbitConnection)
32
+ sleep ::ActivePublisher::RabbitConnection::NETWORK_RECOVERY_INTERVAL
33
+ else
34
+ sleep 0.1
35
+ end
36
+ end
37
+
38
+ def start_thread
39
+ return if alive?
40
+ @thread = ::Thread.new do
41
+ loop do
42
+ # Write "current_messages" so we can requeue should something happen to the consumer.
43
+ @current_messages.concat(queue.pop_up_to(20))
44
+
45
+ begin
46
+ # Only open a single connection for each group of messages to an exchange
47
+ messages_to_retry = []
48
+ @current_messages.group_by(&:exchange_name).each do |exchange_name, messages|
49
+ begin
50
+ ::ActivePublisher.publish_all(exchange_name, messages)
51
+ ensure
52
+ messages_to_retry.concat(messages)
53
+ end
54
+ end
55
+
56
+ # Reset
57
+ @current_messages = []
58
+ rescue *NETWORK_ERRORS
59
+ # Sleep because connection is down
60
+ await_network_reconnect
61
+ @current_messages.concat(messages_to_retry)
62
+
63
+ # Requeue and try again.
64
+ queue.concat(@current_messages) unless @current_messages.empty?
65
+ rescue => unknown_error
66
+ @current_messages.concat(messages_to_retry)
67
+ @current_messages.each do |message|
68
+ # Degrade to single message publish ... or at least attempt to
69
+ begin
70
+ ::ActivePublisher.publish(message.route, message.payload, message.exchange_name, message.options)
71
+ rescue => error
72
+ ::ActivePublisher.configuration.error_handler.call(unknown_error, {:route => message.route, :payload => message.payload, :exchange_name => message.exchange_name, :options => message.options})
73
+ end
74
+ end
75
+
76
+ # Do not requeue the message because something else horrible happened.
77
+ @current_messages = []
78
+
79
+ # TODO: Find a way to bubble this out of the thread for logging purposes.
80
+ # Reraise the error out of the publisher loop. The Supervisor will restart the consumer.
81
+ raise unknown_error
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,148 +1,54 @@
1
+ require "active_publisher/message"
2
+ require "active_publisher/async/in_memory_adapter/async_queue"
3
+ require "active_publisher/async/in_memory_adapter/consumer_thread"
4
+ require "multi_op_queue"
5
+
1
6
  module ActivePublisher
2
7
  module Async
3
- class InMemoryAdapter
4
- include ::ActivePublisher::Logging
5
-
6
- attr_reader :async_queue
7
-
8
- def initialize(drop_messages_when_queue_full = false, max_queue_size = 1_000_000, supervisor_interval = 0.2)
9
- logger.info "Starting in-memory publisher adapter"
10
-
11
- @async_queue = ::ActivePublisher::Async::InMemoryAdapter::AsyncQueue.new(
12
- drop_messages_when_queue_full,
13
- max_queue_size,
14
- supervisor_interval
15
- )
16
- end
8
+ module InMemoryAdapter
17
9
 
18
- def publish(route, payload, exchange_name, options = {})
19
- message = ::ActivePublisher::Async::InMemoryAdapter::Message.new(route, payload, exchange_name, options)
20
- async_queue.push(message)
21
- nil
10
+ def self.new(*args)
11
+ ::ActivePublisher::Async::InMemoryAdapter::Adapter.new(*args)
22
12
  end
23
13
 
24
- def shutdown!
25
- max_wait_time = ::ActivePublisher.configuration.seconds_to_wait_for_graceful_shutdown
26
- started_shutting_down_at = ::Time.now
14
+ class UnableToPersistMessageError < ::StandardError; end
27
15
 
28
- logger.info "Draining async publisher in-memory adapter queue before shutdown. current queue size: #{async_queue.size}."
29
- while async_queue.size > 0
30
- if (::Time.now - started_shutting_down_at) > max_wait_time
31
- logger.info "Forcing async publisher adapter shutdown because graceful shutdown period of #{max_wait_time} seconds was exceeded. Current queue size: #{async_queue.size}."
32
- break
33
- end
34
-
35
- sleep 0.1
36
- end
37
- end
38
-
39
- class AsyncQueue
16
+ class Adapter
40
17
  include ::ActivePublisher::Logging
41
18
 
42
- attr_accessor :drop_messages_when_queue_full,
43
- :max_queue_size,
44
- :supervisor_interval
45
-
46
- attr_reader :consumer, :queue, :supervisor
47
-
48
- if ::RUBY_PLATFORM == "java"
49
- NETWORK_ERRORS = [::MarchHare::Exception, ::Java::ComRabbitmqClient::AlreadyClosedException, ::Java::JavaIo::IOException].freeze
50
- else
51
- NETWORK_ERRORS = [::Bunny::Exception, ::Timeout::Error, ::IOError].freeze
52
- end
53
-
54
- def initialize(drop_messages_when_queue_full, max_queue_size, supervisor_interval)
55
- @drop_messages_when_queue_full = drop_messages_when_queue_full
56
- @max_queue_size = max_queue_size
57
- @supervisor_interval = supervisor_interval
58
- @queue = ::Queue.new
59
- create_and_supervise_consumer!
60
- end
61
-
62
- def push(message)
63
- # default of 1_000_000 messages
64
- if queue.size > max_queue_size
65
- # Drop messages if the queue is full and we were configured to do so
66
- return if drop_messages_when_queue_full
19
+ attr_reader :async_queue
67
20
 
68
- # By default we will raise an error to push the responsibility onto the caller
69
- fail ::ActivePublisher::Async::InMemoryAdapter::UnableToPersistMessageError, "Queue is full, messages will be dropped."
70
- end
21
+ def initialize(drop_messages_when_queue_full = false, max_queue_size = 1_000_000, supervisor_interval = 0.2)
22
+ logger.info "Starting in-memory publisher adapter"
71
23
 
72
- queue.push(message)
24
+ @async_queue = ::ActivePublisher::Async::InMemoryAdapter::AsyncQueue.new(
25
+ drop_messages_when_queue_full,
26
+ max_queue_size,
27
+ supervisor_interval
28
+ )
73
29
  end
74
30
 
75
- def size
76
- queue.size
31
+ def publish(route, payload, exchange_name, options = {})
32
+ message = ::ActivePublisher::Message.new(route, payload, exchange_name, options)
33
+ async_queue.push(message)
34
+ nil
77
35
  end
78
36
 
79
- private
37
+ def shutdown!
38
+ max_wait_time = ::ActivePublisher.configuration.seconds_to_wait_for_graceful_shutdown
39
+ started_shutting_down_at = ::Time.now
80
40
 
81
- def await_network_reconnect
82
- sleep ::ActivePublisher::RabbitConnection::NETWORK_RECOVERY_INTERVAL
83
- end
84
-
85
- def create_and_supervise_consumer!
86
- @consumer = create_consumer
87
- @supervisor = ::Thread.new do
88
- loop do
89
- unless consumer.alive?
90
- # We might need to requeue the last message.
91
- queue.push(@current_message) unless @current_message.nil?
92
- consumer.kill
93
- @consumer = create_consumer
94
- end
95
-
96
- # Pause before checking the consumer again.
97
- sleep supervisor_interval
41
+ logger.info "Draining async publisher in-memory adapter queue before shutdown. current queue size: #{async_queue.size}."
42
+ while async_queue.size > 0
43
+ if (::Time.now - started_shutting_down_at) > max_wait_time
44
+ logger.info "Forcing async publisher adapter shutdown because graceful shutdown period of #{max_wait_time} seconds was exceeded. Current queue size: #{async_queue.size}."
45
+ break
98
46
  end
99
- end
100
- end
101
-
102
- def create_consumer
103
- ::Thread.new do
104
- loop do
105
- # Write "current_message" so we can requeue should something happen to the consumer.
106
- @current_message = message = queue.pop
107
-
108
- begin
109
- ::ActivePublisher.publish(message.route, message.payload, message.exchange_name, message.options)
110
-
111
- # Reset
112
- @current_message = nil
113
- rescue *NETWORK_ERRORS
114
- # Sleep because connection is down
115
- await_network_reconnect
116
47
 
117
- # Requeue and try again.
118
- queue.push(message)
119
- rescue => unknown_error
120
- # Do not requeue the message because something else horrible happened.
121
- @current_message = nil
122
-
123
- ::ActivePublisher.configuration.error_handler.call(unknown_error, {:route => message.route, :payload => message.payload, :exchange_name => message.exchange_name, :options => message.options})
124
-
125
- # TODO: Find a way to bubble this out of the thread for logging purposes.
126
- # Reraise the error out of the publisher loop. The Supervisor will restart the consumer.
127
- raise unknown_error
128
- end
129
- end
48
+ sleep 0.1
130
49
  end
131
50
  end
132
- end
133
-
134
- class Message
135
- attr_reader :route, :payload, :exchange_name, :options
136
-
137
- def initialize(route, payload, exchange_name, options)
138
- @route = route
139
- @payload = payload
140
- @exchange_name = exchange_name
141
- @options = options
142
- end
143
- end
144
51
 
145
- class UnableToPersistMessageError < ::StandardError
146
52
  end
147
53
  end
148
54
  end
@@ -24,6 +24,8 @@ module ActivePublisher
24
24
 
25
25
  @connection = nil
26
26
  end
27
+ rescue Timeout::Error
28
+ # No-op ... this happens sometimes on MRI disconnect
27
29
  end
28
30
 
29
31
  # Private API
@@ -0,0 +1,3 @@
1
+ module ActivePublisher
2
+ class Message < Struct.new(:route, :payload, :exchange_name, :options); end
3
+ end
@@ -1,3 +1,3 @@
1
1
  module ActivePublisher
2
- VERSION = "0.1.5"
2
+ VERSION = "0.2.0.pre"
3
3
  end
@@ -8,11 +8,15 @@ require "thread"
8
8
  require "active_publisher/logging"
9
9
  require "active_publisher/async"
10
10
  require "active_publisher/async/in_memory_adapter"
11
+ require "active_publisher/message"
11
12
  require "active_publisher/version"
12
13
  require "active_publisher/configuration"
13
14
  require "active_publisher/connection"
14
15
 
15
16
  module ActivePublisher
17
+ class UnknownMessageClassError < StandardError; end
18
+ class ExchangeMismatchError < StandardError; end
19
+
16
20
  def self.configuration
17
21
  @configuration ||= ::ActivePublisher::Configuration.new
18
22
  end
@@ -33,6 +37,25 @@ module ActivePublisher
33
37
  end
34
38
  end
35
39
 
40
+ def self.publish_all(exchange_name, messages)
41
+ with_exchange(exchange_name) do |exchange|
42
+ loop do
43
+ break if messages.empty?
44
+ message = messages.shift
45
+
46
+ fail ActivePublisher::UnknownMessageClassError, "bulk publish messages must be ActivePublisher::Message" unless message.is_a?(ActivePublisher::Message)
47
+ fail ActivePublisher::ExchangeMismatchError, "bulk publish messages must match publish_all exchange_name" if message.exchange_name != exchange_name
48
+
49
+ begin
50
+ exchange.publish(message.payload, publishing_options(message.route, message.options || {}))
51
+ rescue
52
+ messages << message
53
+ raise
54
+ end
55
+ end
56
+ end
57
+ end
58
+
36
59
  def self.publishing_options(route, in_options = {})
37
60
  options = {
38
61
  :mandatory => false,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_publisher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0.pre
5
5
  platform: java
6
6
  authors:
7
7
  - Brian Stien
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: exe
14
14
  cert_chain: []
15
- date: 2016-05-09 00:00:00.000000000 Z
15
+ date: 2016-11-17 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  requirement: !ruby/object:Gem::Requirement
@@ -28,6 +28,20 @@ dependencies:
28
28
  - - "~>"
29
29
  - !ruby/object:Gem::Version
30
30
  version: '2.7'
31
+ - !ruby/object:Gem::Dependency
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 0.1.2
37
+ name: multi_op_queue
38
+ prerelease: false
39
+ type: :runtime
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 0.1.2
31
45
  - !ruby/object:Gem::Dependency
32
46
  requirement: !ruby/object:Gem::Requirement
33
47
  requirements:
@@ -109,9 +123,12 @@ files:
109
123
  - lib/active_publisher.rb
110
124
  - lib/active_publisher/async.rb
111
125
  - lib/active_publisher/async/in_memory_adapter.rb
126
+ - lib/active_publisher/async/in_memory_adapter/async_queue.rb
127
+ - lib/active_publisher/async/in_memory_adapter/consumer_thread.rb
112
128
  - lib/active_publisher/configuration.rb
113
129
  - lib/active_publisher/connection.rb
114
130
  - lib/active_publisher/logging.rb
131
+ - lib/active_publisher/message.rb
115
132
  - lib/active_publisher/version.rb
116
133
  homepage: https://github.com/mxenabled/active_publisher
117
134
  licenses:
@@ -128,12 +145,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
128
145
  version: '0'
129
146
  required_rubygems_version: !ruby/object:Gem::Requirement
130
147
  requirements:
131
- - - ">="
148
+ - - ">"
132
149
  - !ruby/object:Gem::Version
133
- version: '0'
150
+ version: 1.3.1
134
151
  requirements: []
135
152
  rubyforge_project:
136
- rubygems_version: 2.6.4
153
+ rubygems_version: 2.6.7
137
154
  signing_key:
138
155
  specification_version: 4
139
156
  summary: Aims to make publishing work across MRI and jRuby painless and add some nice features like automatially publishing lifecycle events for ActiveRecord models.