protobuf-nats 0.1.3 → 0.2.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.
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: