active_publisher 0.1.5-java → 0.2.0.pre-java

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