protobuf-nats 0.1.3 → 0.2.0

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: 6da90fadf74510112e08d690f747aa2f1dbebb1e
4
- data.tar.gz: dde2e57a8cdc900f029e8931cd6153b6e3bf6904
3
+ metadata.gz: eeff2a5c3d939012fe3cd2cb7cce12ba89392a81
4
+ data.tar.gz: 406a4b1f779d1a3d91ea136bbb3616560e8152a7
5
5
  SHA512:
6
- metadata.gz: 4339ab9bac17df4f2e7b5f33ef5ba93fc4c6e4896c7d84fbd319df503e3790cd6927fbc2c241779fc66c1f7c77438d9b298b923038edf9bfe017ae52274f923c
7
- data.tar.gz: 71ff1d34845258b508960d7b5ca8a6ca218d02659ecf4a8513a14d9420a09d44a190a4149822679fbd37b810e6fedde89d3b4cfec56f3880832a5713cb805e70
6
+ metadata.gz: 8a517022291c46d42a4c6b243cc50625ca37770e64c3fb387c990b43d06b70078ab697a2b153b1e731f04d622758c4a7a33915267014f0f47440e238f3d1455e
7
+ data.tar.gz: 2368ebed1aead198a600eebac3cdea821c4004737a9983e549b217025fb7d1d10c4a3bed46523b189b1920bb872af2e462acf61d00b07f07c1f264292bd433cb
data/bench/real_client.rb CHANGED
@@ -4,10 +4,6 @@ ENV["PB_SERVER_TYPE"] = "protobuf/nats/runner"
4
4
  require "benchmark/ips"
5
5
  require "./examples/warehouse/app"
6
6
 
7
- payload = ::Warehouse::Shipment.new(:guid => ::SecureRandom.uuid, :address => "123 yolo st")
8
- nats = ::NATS::IO::Client.new
9
- nats.connect({:servers => ["nats://127.0.0.1:4222"]})
10
-
11
7
  Protobuf::Logging.logger = ::Logger.new(nil)
12
8
 
13
9
  Benchmark.ips do |config|
Binary file
Binary file
Binary file
Binary file
data/lib/protobuf/nats.rb CHANGED
@@ -4,7 +4,6 @@ require "protobuf"
4
4
  # We don't need this, but the CLI attempts to terminate.
5
5
  require "protobuf/rpc/service_directory"
6
6
 
7
- require "concurrent"
8
7
  require "nats/io/client"
9
8
 
10
9
  require "protobuf/nats/client"
@@ -22,6 +21,13 @@ module Protobuf
22
21
  ACK = "\1".freeze
23
22
  end
24
23
 
24
+ NatsClient = if defined? JRUBY_VERSION
25
+ require "protobuf/nats/jnats"
26
+ ::Protobuf::Nats::JNats
27
+ else
28
+ ::NATS::IO::Client
29
+ end
30
+
25
31
  GET_CONNECTED_MUTEX = ::Mutex.new
26
32
 
27
33
  def self.config
@@ -46,16 +52,39 @@ module Protobuf
46
52
  GET_CONNECTED_MUTEX.synchronize do
47
53
  return if @start_client_nats_connection
48
54
 
49
- @client_nats_connection = ::NATS::IO::Client.new
55
+ @client_nats_connection = NatsClient.new
50
56
  @client_nats_connection.connect(config.connection_options)
51
57
 
52
58
  # Ensure we have a valid connection to the NATS server.
53
59
  @client_nats_connection.flush(5)
54
60
 
61
+ @client_nats_connection.on_error do |error|
62
+ log_error(error)
63
+ end
64
+
55
65
  true
56
66
  end
57
67
  end
58
68
  end
59
69
 
70
+ # This will work with both ruby and java errors
71
+ def self.log_error(error)
72
+ logger.error error.to_s
73
+ logger.error error.class.to_s
74
+ if error.respond_to?(:backtrace) && error.backtrace.is_a?(::Array)
75
+ logger.error error.backtrace.join("\n")
76
+ end
77
+ end
78
+
79
+ def self.logger
80
+ ::Protobuf::Logging.logger
81
+ end
82
+
83
+ logger.info "Using #{NatsClient} to connect"
84
+
85
+ at_exit do
86
+ ::Protobuf::Nats.client_nats_connection.close rescue nil
87
+ end
88
+
60
89
  end
61
90
  end
@@ -44,55 +44,106 @@ module Protobuf
44
44
  end
45
45
  end
46
46
 
47
- # This is a request that expects two responses.
48
- # 1. An ACK from the server. We use a shorter timeout.
49
- # 2. A PB message from the server. We use a longer timoeut.
50
- def nats_request_with_two_responses(subject, data, opts)
51
- nats = Protobuf::Nats.client_nats_connection
52
- inbox = nats.new_inbox
53
- lock = ::Monitor.new
54
- ack_condition = lock.new_cond
55
- pb_response_condition = lock.new_cond
56
- response = nil
57
- sid = nats.subscribe(inbox, :max => 2) do |message|
58
- lock.synchronize do
59
- case message
60
- when ::Protobuf::Nats::Messages::ACK
61
- ack_condition.signal
62
- next
63
- else
64
- response = message
65
- pb_response_condition.signal
66
- end
67
- end
68
- end
69
-
70
- lock.synchronize do
71
- # Publish to server
72
- nats.publish(subject, data, inbox)
47
+ # The Java nats client offers better message queueing so we're going to use
48
+ # that over locking ourselves. This split in code isn't great, but we can
49
+ # refactor this later.
50
+ if defined? JRUBY_VERSION
73
51
 
52
+ # This is a request that expects two responses.
53
+ # 1. An ACK from the server. We use a shorter timeout.
54
+ # 2. A PB message from the server. We use a longer timoeut.
55
+ def nats_request_with_two_responses(subject, data, opts)
74
56
  # Wait for the ACK from the server
75
57
  ack_timeout = opts[:ack_timeout] || 5
76
- with_timeout(ack_timeout) { ack_condition.wait(ack_timeout) }
77
-
78
58
  # Wait for the protobuf response
79
59
  timeout = opts[:timeout] || 60
80
- with_timeout(timeout) { pb_response_condition.wait(timeout) } unless response
60
+
61
+ nats = ::Protobuf::Nats.client_nats_connection
62
+ inbox = nats.new_inbox
63
+
64
+ # Publish to server
65
+ sub = nats.subscribe(inbox, :max => 2)
66
+ nats.publish(subject, data, inbox)
67
+
68
+ # Wait for reply
69
+ first_message = nats.next_message(sub, ack_timeout)
70
+ fail ::NATS::IO::Timeout if first_message.nil?
71
+ second_message = nats.next_message(sub, timeout)
72
+ fail ::NATS::IO::Timeout if second_message.nil?
73
+
74
+ # Check messages
75
+ response = nil
76
+ has_ack = false
77
+ case first_message.data
78
+ when ::Protobuf::Nats::Messages::ACK then has_ack = true
79
+ else response = first_message.data
80
+ end
81
+ case second_message.data
82
+ when ::Protobuf::Nats::Messages::ACK then has_ack = true
83
+ else response = second_message.data
84
+ end
85
+
86
+ success = has_ack && response
87
+ fail ::NATS::IO::Timeout unless success
88
+
89
+ response
90
+ ensure
91
+ # Ensure we don't leave a subscriptiosn sitting around.
92
+ # This also cleans up memory. It's a no-op if the subscription
93
+ # is already cleaned up.
94
+ nats.unsubscribe(sub)
81
95
  end
82
96
 
83
- response
84
- ensure
85
- # Ensure we don't leave a subscription sitting around.
86
- nats.unsubscribe(sid) if response.nil?
87
- end
97
+ else
98
+
99
+ def nats_request_with_two_responses(subject, data, opts)
100
+ nats = Protobuf::Nats.client_nats_connection
101
+ inbox = nats.new_inbox
102
+ lock = ::Monitor.new
103
+ ack_condition = lock.new_cond
104
+ pb_response_condition = lock.new_cond
105
+ response = nil
106
+ sid = nats.subscribe(inbox, :max => 2) do |message, _, _|
107
+ lock.synchronize do
108
+ case message
109
+ when ::Protobuf::Nats::Messages::ACK
110
+ ack_condition.signal
111
+ next
112
+ else
113
+ response = message
114
+ pb_response_condition.signal
115
+ end
116
+ end
117
+ end
118
+
119
+ lock.synchronize do
120
+ # Publish to server
121
+ nats.publish(subject, data, inbox)
122
+
123
+ # Wait for the ACK from the server
124
+ ack_timeout = opts[:ack_timeout] || 5
125
+ with_timeout(ack_timeout) { ack_condition.wait(ack_timeout) }
126
+
127
+ # Wait for the protobuf response
128
+ timeout = opts[:timeout] || 60
129
+ with_timeout(timeout) { pb_response_condition.wait(timeout) } unless response
130
+ end
131
+
132
+ response
133
+ ensure
134
+ # Ensure we don't leave a subscription sitting around.
135
+ nats.unsubscribe(sid) if response.nil?
136
+ end
137
+
138
+ # This is a copy of #with_nats_timeout
139
+ def with_timeout(timeout)
140
+ start_time = ::NATS::MonotonicTime.now
141
+ yield
142
+ end_time = ::NATS::MonotonicTime.now
143
+ duration = end_time - start_time
144
+ raise ::NATS::IO::Timeout.new("nats: timeout") if duration > timeout
145
+ end
88
146
 
89
- # This is a copy of #with_nats_timeout
90
- def with_timeout(timeout)
91
- start_time = ::NATS::MonotonicTime.now
92
- yield
93
- end_time = ::NATS::MonotonicTime.now
94
- duration = end_time - start_time
95
- raise ::NATS::IO::Timeout.new("nats: timeout") if duration > timeout
96
147
  end
97
148
 
98
149
  end
@@ -4,7 +4,7 @@ require "yaml"
4
4
  module Protobuf
5
5
  module Nats
6
6
  class Config
7
- attr_accessor :uses_tls, :servers, :connect_timeout, :tls_client_cert, :tls_client_key
7
+ attr_accessor :uses_tls, :servers, :connect_timeout, :tls_client_cert, :tls_client_key, :tls_ca_cert
8
8
 
9
9
  CONFIG_MUTEX = ::Mutex.new
10
10
 
@@ -13,6 +13,7 @@ module Protobuf
13
13
  :servers => nil,
14
14
  :tls_client_cert => nil,
15
15
  :tls_client_key => nil,
16
+ :tls_ca_cert => nil,
16
17
  :uses_tls => false,
17
18
  }.freeze
18
19
 
@@ -53,6 +54,10 @@ module Protobuf
53
54
  @connection_options ||= begin
54
55
  options = {
55
56
  servers: servers,
57
+ uses_tls: uses_tls,
58
+ tls_client_cert: tls_client_cert,
59
+ tls_client_key: tls_client_key,
60
+ tls_ca_cert: tls_ca_cert,
56
61
  connect_timeout: connect_timeout
57
62
  }
58
63
  options[:tls] = {:context => new_tls_context} if uses_tls
@@ -0,0 +1,239 @@
1
+ ext_base = ::File.join(::File.dirname(__FILE__), '..', '..', '..', 'ext')
2
+
3
+ require ::File.join(ext_base, "jars/slf4j-api-1.7.25.jar")
4
+ require ::File.join(ext_base, "jars/slf4j-simple-1.7.25.jar")
5
+ require ::File.join(ext_base, "jars/gson-2.6.2.jar")
6
+ require ::File.join(ext_base, "jars/jnats-1.1-SNAPSHOT.jar")
7
+
8
+ # Set field accessors so we can access the member variables directly.
9
+ class Java::IoNatsClient::SubscriptionImpl
10
+ field_accessor :pMsgs, :pBytes, :delivered
11
+ end
12
+
13
+ module Protobuf
14
+ module Nats
15
+ class JNats
16
+ attr_reader :connection
17
+
18
+ class Message
19
+ attr_reader :data, :subject, :reply
20
+
21
+ def initialize(nats_message)
22
+ @data = nats_message.getData.to_s
23
+ @reply = nats_message.getReplyTo.to_s
24
+ @subject = nats_message.getSubject
25
+ end
26
+ end
27
+
28
+ def initialize
29
+ @on_error_cb = lambda {|error|}
30
+ @on_reconnect_cb = lambda {}
31
+ @on_disconnect_cb = lambda {}
32
+ @on_close_cb = lambda {}
33
+ @subz_cbs = {}
34
+ @subz_mutex = ::Mutex.new
35
+ end
36
+
37
+ def connect(options = {})
38
+ servers = options[:servers] || ["nats://localhost:4222"]
39
+ servers = [servers].flatten.map { |uri_string| java.net.URI.new(uri_string) }
40
+ connection_factory = ::Java::IoNatsClient::ConnectionFactory.new
41
+ connection_factory.setServers(servers)
42
+ # Basically never stop trying to connect
43
+ connection_factory.setMaxReconnect(60_000)
44
+
45
+ # Setup callbacks
46
+ connection_factory.setDisconnectedCallback { |event| @on_disconnect_cb.call }
47
+ connection_factory.setReconnectedCallback { |_event| @on_reconnect_cb.call }
48
+ connection_factory.setClosedCallback { |_event| @on_close_cb.call }
49
+ connection_factory.setExceptionHandler { |error| @on_error_cb.call(error) }
50
+
51
+ # Setup ssl context if we're using tls
52
+ if options[:uses_tls]
53
+ ssl_context = create_ssl_context(options)
54
+ connection_factory.setSecure(true)
55
+ connection_factory.setSSLContext(ssl_context)
56
+ end
57
+
58
+ @connection = connection_factory.createConnection
59
+
60
+ # We're going to spawn a consumer and supervisor
61
+ @work_queue = @connection.createMsgChannel
62
+ spwan_supervisor_and_consumer
63
+
64
+ @connection
65
+ end
66
+
67
+ # Do not depend on #close for a greaceful disconnect.
68
+ def close
69
+ @connection.close
70
+ @supervisor.kill rescue nil
71
+ @consumer.kill rescue nil
72
+ end
73
+
74
+ def flush(timeout_sec = 0.5)
75
+ @connection.flush(timeout_sec * 1000)
76
+ end
77
+
78
+ def next_message(sub, timeout_sec)
79
+ nats_message = sub.nextMessage(timeout_sec * 1000)
80
+ return nil unless nats_message
81
+ Message.new(nats_message)
82
+ end
83
+
84
+ def publish(subject, data, mailbox = nil)
85
+ # The "true" here is to force flush. May not need this.
86
+ @connection.publish(subject, mailbox, data.to_java_bytes, true)
87
+ end
88
+
89
+ def subscribe(subject, options = {}, &block)
90
+ queue = options[:queue]
91
+ max = options[:max]
92
+ work_queue = nil
93
+ # We pass our work queue for processing async work because java nats
94
+ # uses a cahced thread pool: 1 thread per async subscription.
95
+ # Sync subs need their own queue so work is not processed async.
96
+ work_queue = block.nil? ? @connection.createMsgChannel : @work_queue
97
+ sub = @connection.subscribe(subject, queue, nil, work_queue)
98
+
99
+ # Register the block callback. We only lock to save the callback.
100
+ if block
101
+ @subz_mutex.synchronize do
102
+ @subz_cbs[sub.getSid] = block
103
+ end
104
+ end
105
+
106
+ # Auto unsub if max message option was provided.
107
+ sub.autoUnsubscribe(max) if max
108
+
109
+ sub
110
+ end
111
+
112
+ def unsubscribe(sub)
113
+ return if sub.nil?
114
+
115
+ # Cleanup our async callback
116
+ if @subz_cbs[sub.getSid]
117
+ @subz_mutex.synchronize do
118
+ @subz_cbs.delete(sub.getSid)
119
+ end
120
+ end
121
+
122
+ # The "true" here is to ignore and invalid conn.
123
+ sub.unsubscribe(true)
124
+ end
125
+
126
+ def new_inbox
127
+ "_INBOX.#{::SecureRandom.hex(13)}"
128
+ end
129
+
130
+ def on_reconnect(&cb)
131
+ @on_reconnect_cb = cb
132
+ end
133
+
134
+ def on_disconnect(&cb)
135
+ @on_disconnect_cb = cb
136
+ end
137
+
138
+ def on_error(&cb)
139
+ @on_error_cb = cb
140
+ end
141
+
142
+ def on_close(&cb)
143
+ @on_close_cb = cb
144
+ end
145
+
146
+ private
147
+
148
+ def spwan_supervisor_and_consumer
149
+ spawn_consumer
150
+ @supervisor = ::Thread.new do
151
+ loop do
152
+ begin
153
+ sleep 1
154
+ next if @consumer && @consumer.alive?
155
+ # We need to recreate the consumer thread
156
+ @consumer.kill if @consumer
157
+ spawn_consumer
158
+ rescue => error
159
+ @on_error_cb.call(error)
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ def spawn_consumer
166
+ @consumer = ::Thread.new do
167
+ loop do
168
+ begin
169
+ message = @work_queue.take
170
+ next unless message
171
+ sub = message.getSubscription
172
+
173
+ # We have to update the subscription stats so we're not considered a slow consumer.
174
+ begin
175
+ sub.lock
176
+ sub.pMsgs -= 1
177
+ sub.pBytes -= message.getData.length if message.getData
178
+ sub.delivered += 1 unless sub.isClosed
179
+ ensure
180
+ sub.unlock
181
+ end
182
+
183
+ # We don't need t
184
+ callback = @subz_cbs[sub.getSid]
185
+ next unless callback
186
+ callback.call(message.getData.to_s, message.getReplyTo, message.getSubject)
187
+ rescue => error
188
+ @on_error_cb.call(error)
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ # Jruby-openssl depends on bouncycastle so our lives don't suck super bad
195
+ def read_pem_object_from_file(path)
196
+ fail ::ArgumentError, "Tried to read a PEM key or cert with path nil" if path.nil?
197
+
198
+ file_reader = java.io.FileReader.new(path)
199
+ pem_parser = org.bouncycastle.openssl.PEMParser.new(file_reader)
200
+ object = pem_parser.readObject
201
+ pem_parser.close
202
+ object
203
+ end
204
+
205
+ def create_ssl_context(options)
206
+ # Create our certs and key converters to go from bouncycastle to java.
207
+ cert_converter = org.bouncycastle.cert.jcajce.JcaX509CertificateConverter.new
208
+ key_converter = org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter.new
209
+
210
+ # Load the certs and keys.
211
+ tls_ca_cert = cert_converter.getCertificate(read_pem_object_from_file(options[:tls_ca_cert]))
212
+ tls_client_cert = cert_converter.getCertificate(read_pem_object_from_file(options[:tls_client_cert]))
213
+ tls_client_key = key_converter.getKeyPair(read_pem_object_from_file(options[:tls_client_key]))
214
+
215
+ # Setup the CA cert.
216
+ ca_key_store = java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType)
217
+ ca_key_store.load(nil, nil)
218
+ ca_key_store.setCertificateEntry("ca-certificate", tls_ca_cert)
219
+ trust_manager = javax.net.ssl.TrustManagerFactory.getInstance(javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm)
220
+ trust_manager.init(ca_key_store)
221
+
222
+ # Setup the cert / key pair.
223
+ client_key_store = java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType)
224
+ client_key_store.load(nil, nil)
225
+ client_key_store.setCertificateEntry("certificate", tls_client_cert)
226
+ certificate_java_array = [tls_client_cert].to_java(java.security.cert.Certificate)
227
+ empty_password = [].to_java(:char)
228
+ client_key_store.setKeyEntry("private-key", tls_client_key.getPrivate, empty_password, certificate_java_array)
229
+ key_manager = javax.net.ssl.KeyManagerFactory.getInstance(javax.net.ssl.KeyManagerFactory.getDefaultAlgorithm)
230
+ key_manager.init(client_key_store, empty_password)
231
+
232
+ # Create ssl context.
233
+ context = javax.net.ssl.SSLContext.getInstance("TLSv1.2")
234
+ context.init(key_manager.getKeyManagers, trust_manager.getTrustManagers, nil)
235
+ context
236
+ end
237
+ end
238
+ end
239
+ end
@@ -1,6 +1,7 @@
1
1
  require "active_support/core_ext/class/subclasses"
2
2
  require "protobuf/rpc/server"
3
3
  require "protobuf/rpc/service"
4
+ require "protobuf/nats/thread_pool"
4
5
 
5
6
  module Protobuf
6
7
  module Nats
@@ -15,10 +16,10 @@ module Protobuf
15
16
  @running = true
16
17
  @stopped = false
17
18
 
18
- @nats = options[:client] || ::NATS::IO::Client.new
19
+ @nats = options[:client] || ::Protobuf::Nats::NatsClient.new
19
20
  @nats.connect(::Protobuf::Nats.config.connection_options)
20
21
 
21
- @thread_pool = ::Concurrent::FixedThreadPool.new(options[:threads], :max_queue => options[:threads])
22
+ @thread_pool = ::Protobuf::Nats::ThreadPool.new(options[:threads], :max_queue => options[:threads])
22
23
 
23
24
  @subscriptions = []
24
25
  end
@@ -27,29 +28,22 @@ module Protobuf
27
28
  ::Protobuf::Rpc::Service.implemented_services.map(&:safe_constantize)
28
29
  end
29
30
 
30
- def execute_request_promise(request_data, reply_id)
31
- promise = ::Concurrent::Promise.new(:executor => thread_pool).then do
32
- # Process request.
33
- response_data = handle_request(request_data)
34
- # Publish response.
35
- nats.publish(reply_id, response_data)
36
- end.on_error do |error|
37
- log_error(error)
38
- end.execute
31
+ def enqueue_request(request_data, reply_id)
32
+ was_enqueued = thread_pool.push do
33
+ begin
34
+ # Process request.
35
+ response_data = handle_request(request_data)
36
+ # Publish response.
37
+ nats.publish(reply_id, response_data)
38
+ rescue => error
39
+ ::Protobuf::Nats.log_error(error)
40
+ end
41
+ end
39
42
 
40
43
  # Publish an ACK to signal the server has picked up the work.
41
- nats.publish(reply_id, ::Protobuf::Nats::Messages::ACK)
44
+ nats.publish(reply_id, ::Protobuf::Nats::Messages::ACK) if was_enqueued
42
45
 
43
- promise
44
- rescue ::Concurrent::RejectedExecutionError
45
- nil
46
- end
47
-
48
- def log_error(error)
49
- logger.error error.to_s
50
- if error.respond_to?(:backtrace) && error.backtrace.is_a?(::Array)
51
- logger.error error.backtrace.join("\n")
52
- end
46
+ was_enqueued
53
47
  end
54
48
 
55
49
  def subscribe_to_services
@@ -64,7 +58,7 @@ module Protobuf
64
58
  logger.info " - #{subscription_key_and_queue}"
65
59
 
66
60
  subscriptions << nats.subscribe(subscription_key_and_queue, :queue => subscription_key_and_queue) do |request_data, reply_id, _subject|
67
- unless execute_request_promise(request_data, reply_id)
61
+ unless enqueue_request(request_data, reply_id)
68
62
  logger.error { "Thread pool is full! Dropping message for: #{subscription_key_and_queue}" }
69
63
  end
70
64
  end
@@ -82,7 +76,7 @@ module Protobuf
82
76
  end
83
77
 
84
78
  nats.on_error do |error|
85
- log_error(error)
79
+ ::Protobuf::Nats.log_error(error)
86
80
  end
87
81
 
88
82
  nats.on_close do
@@ -0,0 +1,108 @@
1
+ module Protobuf
2
+ module Nats
3
+ class ThreadPool
4
+
5
+ def initialize(size, opts = {})
6
+ @queue = ::Queue.new
7
+ @active_work = 0
8
+
9
+ # Callbacks
10
+ @error_cb = lambda {|_error|}
11
+
12
+ # Synchronization
13
+ @mutex = ::Mutex.new
14
+ @cb_mutex = ::Mutex.new
15
+
16
+ # Let's get this party started
17
+ queue_size = opts[:max_queue].to_i || 0
18
+ @max_size = size + queue_size
19
+ @max_workers = size
20
+ @shutting_down = false
21
+ @workers = []
22
+ supervise_workers
23
+ end
24
+
25
+ def full?
26
+ @active_work >= @max_size
27
+ end
28
+
29
+ # This method is not thread safe by design since our IO model is a single producer thread
30
+ # with multiple consumer threads.
31
+ def push(&work_cb)
32
+ return false if full?
33
+ return false if @shutting_down
34
+ @queue << [:work, work_cb]
35
+ @mutex.synchronize { @active_work += 1 }
36
+ supervise_workers
37
+ true
38
+ end
39
+
40
+ # This method is not thread safe by design since our IO model is a single producer thread
41
+ # with multiple consumer threads.
42
+ def shutdown
43
+ @shutting_down = true
44
+ @max_workers.times { @queue << [:stop, nil] }
45
+ end
46
+
47
+ def kill
48
+ @shutting_down = true
49
+ @workers.map(&:kill)
50
+ end
51
+
52
+ # This method is not thread safe by design since our IO model is a single producer thread
53
+ # with multiple consumer threads.
54
+ def wait_for_termination(seconds = nil)
55
+ started_at = ::Time.now
56
+ loop do
57
+ sleep 0.1
58
+ break if seconds && (::Time.now - started_at) >= seconds
59
+ break if @workers.empty?
60
+ prune_dead_workers
61
+ end
62
+ end
63
+
64
+ # This callback is executed in a thread safe manner.
65
+ def on_error(&cb)
66
+ @error_cb = cb
67
+ end
68
+
69
+ def size
70
+ @active_work
71
+ end
72
+
73
+ private
74
+
75
+ def prune_dead_workers
76
+ @workers = @workers.select(&:alive?)
77
+ end
78
+
79
+ def supervise_workers
80
+ prune_dead_workers
81
+ missing_worker_count = (@max_workers - @workers.size)
82
+ missing_worker_count.times do
83
+ @workers << spawn_worker
84
+ end
85
+ end
86
+
87
+ def spawn_worker
88
+ ::Thread.new do
89
+ loop do
90
+ type, cb = @queue.pop
91
+ begin
92
+ # Break if we're shutting down
93
+ break if type == :stop
94
+ # Perform work
95
+ cb.call
96
+ # Update stats
97
+ rescue => error
98
+ @cb_mutex.synchronize { @error_cb.call(error) }
99
+ ensure
100
+ @mutex.synchronize { @active_work -= 1 }
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -1,5 +1,5 @@
1
1
  module Protobuf
2
2
  module Nats
3
- VERSION = "0.1.3"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -31,7 +31,6 @@ Gem::Specification.new do |spec|
31
31
  spec.require_paths = ["lib"]
32
32
 
33
33
  spec.add_runtime_dependency "protobuf", "~> 3.7"
34
- spec.add_runtime_dependency "concurrent-ruby"
35
34
  spec.add_runtime_dependency "nats-pure"
36
35
 
37
36
  spec.add_development_dependency "bundler", "~> 1.14"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protobuf-nats
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Dewitt
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-03-30 00:00:00.000000000 Z
11
+ date: 2017-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: protobuf
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.7'
27
- - !ruby/object:Gem::Dependency
28
- name: concurrent-ruby
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: nats-pure
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -144,11 +130,17 @@ files:
144
130
  - examples/warehouse/app.rb
145
131
  - examples/warehouse/start_client.sh
146
132
  - examples/warehouse/start_server.sh
133
+ - ext/jars/gson-2.6.2.jar
134
+ - ext/jars/jnats-1.1-SNAPSHOT.jar
135
+ - ext/jars/slf4j-api-1.7.25.jar
136
+ - ext/jars/slf4j-simple-1.7.25.jar
147
137
  - lib/protobuf/nats.rb
148
138
  - lib/protobuf/nats/client.rb
149
139
  - lib/protobuf/nats/config.rb
140
+ - lib/protobuf/nats/jnats.rb
150
141
  - lib/protobuf/nats/runner.rb
151
142
  - lib/protobuf/nats/server.rb
143
+ - lib/protobuf/nats/thread_pool.rb
152
144
  - lib/protobuf/nats/version.rb
153
145
  - protobuf-nats.gemspec
154
146
  homepage: