nats-pure 2.3.0 → 2.5.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/README.md +10 -3
  4. data/lib/nats/client.rb +7 -2
  5. data/lib/nats/io/client.rb +304 -282
  6. data/lib/nats/io/errors.rb +2 -0
  7. data/lib/nats/io/jetstream/api.rb +54 -47
  8. data/lib/nats/io/jetstream/errors.rb +30 -14
  9. data/lib/nats/io/jetstream/js/config.rb +9 -3
  10. data/lib/nats/io/jetstream/js/header.rb +15 -9
  11. data/lib/nats/io/jetstream/js/status.rb +11 -5
  12. data/lib/nats/io/jetstream/js/sub.rb +4 -2
  13. data/lib/nats/io/jetstream/js.rb +10 -8
  14. data/lib/nats/io/jetstream/manager.rb +104 -83
  15. data/lib/nats/io/jetstream/msg/ack.rb +15 -9
  16. data/lib/nats/io/jetstream/msg/ack_methods.rb +24 -22
  17. data/lib/nats/io/jetstream/msg/metadata.rb +9 -7
  18. data/lib/nats/io/jetstream/msg.rb +11 -4
  19. data/lib/nats/io/jetstream/pull_subscription.rb +21 -10
  20. data/lib/nats/io/jetstream/push_subscription.rb +3 -1
  21. data/lib/nats/io/jetstream.rb +125 -54
  22. data/lib/nats/io/kv/api.rb +7 -3
  23. data/lib/nats/io/kv/bucket_status.rb +7 -5
  24. data/lib/nats/io/kv/errors.rb +25 -2
  25. data/lib/nats/io/kv/manager.rb +19 -10
  26. data/lib/nats/io/kv.rb +359 -22
  27. data/lib/nats/io/msg.rb +19 -19
  28. data/lib/nats/io/parser.rb +23 -23
  29. data/lib/nats/io/rails.rb +2 -0
  30. data/lib/nats/io/subscription.rb +25 -22
  31. data/lib/nats/io/version.rb +4 -2
  32. data/lib/nats/io/websocket.rb +10 -8
  33. data/lib/nats/nuid.rb +33 -22
  34. data/lib/nats/service/callbacks.rb +22 -0
  35. data/lib/nats/service/endpoint.rb +155 -0
  36. data/lib/nats/service/errors.rb +44 -0
  37. data/lib/nats/service/group.rb +37 -0
  38. data/lib/nats/service/monitoring.rb +108 -0
  39. data/lib/nats/service/stats.rb +52 -0
  40. data/lib/nats/service/status.rb +66 -0
  41. data/lib/nats/service/validator.rb +31 -0
  42. data/lib/nats/service.rb +121 -0
  43. data/lib/nats/utils/list.rb +26 -0
  44. data/lib/nats-pure.rb +5 -0
  45. data/lib/nats.rb +10 -6
  46. metadata +176 -5
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright 2016-2021 The NATS Authors
2
4
  # Licensed under the Apache License, Version 2.0 (the "License");
3
5
  # you may not use this file except in compliance with the License.
@@ -13,45 +15,46 @@
13
15
  #
14
16
 
15
17
  module NATS
16
-
17
18
  # A Subscription represents interest in a given subject.
18
- #
19
+ #
19
20
  # @example Create NATS subscription with callback.
20
21
  # require 'nats/client'
21
- #
22
+ #
22
23
  # nc = NATS.connect("demo.nats.io")
23
24
  # sub = nc.subscribe("foo") do |msg|
24
25
  # puts "Received [#{msg.subject}]: #{}"
25
26
  # end
26
- #
27
+ #
27
28
  class Subscription
28
29
  include MonitorMixin
29
30
 
30
31
  attr_accessor :subject, :queue, :future, :callback, :response, :received, :max, :pending, :sid
31
- attr_accessor :pending_queue, :pending_size, :wait_for_msgs_cond, :concurrency_semaphore
32
+ attr_accessor :pending_queue, :pending_size, :wait_for_msgs_cond
32
33
  attr_accessor :pending_msgs_limit, :pending_bytes_limit
33
34
  attr_accessor :nc
34
35
  attr_accessor :jsi
35
- attr_accessor :closed
36
+ attr_accessor :closed, :drained
37
+ alias_method :delivered, :received
36
38
 
37
39
  def initialize(**opts)
38
40
  super() # required to initialize monitor
39
- @subject = ''
40
- @queue = nil
41
- @future = nil
41
+ @subject = ""
42
+ @queue = nil
43
+ @future = nil
42
44
  @callback = nil
43
45
  @response = nil
44
46
  @received = 0
45
- @max = nil
46
- @pending = nil
47
- @sid = nil
48
- @nc = nil
49
- @closed = nil
47
+ @max = nil
48
+ @pending = nil
49
+ @sid = nil
50
+ @nc = nil
51
+ @closed = nil
52
+ @drained = false
50
53
 
51
54
  # State from async subscriber messages delivery
52
- @pending_queue = nil
53
- @pending_size = 0
54
- @pending_msgs_limit = nil
55
+ @pending_queue = nil
56
+ @pending_size = 0
57
+ @pending_msgs_limit = nil
55
58
  @pending_bytes_limit = nil
56
59
 
57
60
  # Sync subscriber
@@ -78,22 +81,22 @@ module NATS
78
81
 
79
82
  # Auto unsubscribes the server by sending UNSUB command and throws away
80
83
  # subscription in case already present and has received enough messages.
81
- def unsubscribe(opt_max=nil)
84
+ def unsubscribe(opt_max = nil)
82
85
  @nc.send(:unsubscribe, self, opt_max)
83
86
  end
84
87
 
85
88
  # next_msg blocks and waiting for the next message to be received.
86
- def next_msg(opts={})
89
+ def next_msg(opts = {})
87
90
  timeout = opts[:timeout] ||= 0.5
88
91
  synchronize do
89
- return @pending_queue.pop if not @pending_queue.empty?
92
+ return @pending_queue.pop if !@pending_queue.empty?
90
93
 
91
94
  # Wait for a bit until getting a signal.
92
- MonotonicTime::with_nats_timeout(timeout) do
95
+ MonotonicTime.with_nats_timeout(timeout) do
93
96
  wait_for_msgs_cond.wait(timeout)
94
97
  end
95
98
 
96
- if not @pending_queue.empty?
99
+ if !@pending_queue.empty?
97
100
  return @pending_queue.pop
98
101
  else
99
102
  raise NATS::Timeout
@@ -1,4 +1,6 @@
1
- # Copyright 2016-2022 The NATS Authors
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2016-2025 The NATS Authors
2
4
  # Licensed under the Apache License, Version 2.0 (the "License");
3
5
  # you may not use this file except in compliance with the License.
4
6
  # You may obtain a copy of the License at
@@ -15,7 +17,7 @@
15
17
  module NATS
16
18
  module IO
17
19
  # VERSION is the version of the client announced on CONNECT to the server.
18
- VERSION = "2.3.0".freeze
20
+ VERSION = "2.5.0"
19
21
 
20
22
  # LANG is the lang runtime of the client announced on CONNECT to the server.
21
23
  LANG = "#{RUBY_ENGINE}#{RUBY_VERSION}".freeze
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
- require 'websocket'
4
+ require "websocket"
3
5
  rescue LoadError
4
6
  raise LoadError, "Please add `websocket` gem to your Gemfile to connect to NATS via WebSocket."
5
7
  end
@@ -15,7 +17,7 @@ module NATS
15
17
 
16
18
  attr_accessor :socket
17
19
 
18
- def initialize(options={})
20
+ def initialize(options = {})
19
21
  super
20
22
  end
21
23
 
@@ -44,31 +46,31 @@ module NATS
44
46
  super
45
47
  end
46
48
 
47
- def read(max_bytes=MAX_SOCKET_READ_BYTES, deadline=nil)
49
+ def read(max_bytes = MAX_SOCKET_READ_BYTES, deadline = nil)
48
50
  data = super
49
51
  @frame << data
50
52
  result = []
51
- while msg = @frame.next
53
+ while (msg = @frame.next)
52
54
  result << msg
53
55
  end
54
56
  result.join
55
57
  end
56
58
 
57
- def read_line(deadline=nil)
59
+ def read_line(deadline = nil)
58
60
  data = super
59
61
  @frame << data
60
62
  result = []
61
- while msg = @frame.next
63
+ while (msg = @frame.next)
62
64
  result << msg
63
65
  end
64
66
  result.join
65
67
  end
66
68
 
67
- def write(data, deadline=nil)
69
+ def write(data, deadline = nil)
68
70
  raise HandshakeError, "Attempted to write to socket while WebSocket handshake is in progress" unless @handshaked
69
71
 
70
72
  frame = ::WebSocket::Frame::Outgoing::Client.new(data: data, type: :binary, version: @handshake.version)
71
- super frame.to_s
73
+ super(frame.to_s)
72
74
  end
73
75
  end
74
76
  end
data/lib/nats/nuid.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright 2016-2018 The NATS Authors
2
4
  # Licensed under the Apache License, Version 2.0 (the "License");
3
5
  # you may not use this file except in compliance with the License.
@@ -11,27 +13,27 @@
11
13
  # See the License for the specific language governing permissions and
12
14
  # limitations under the License.
13
15
  #
14
- require 'securerandom'
16
+ require "securerandom"
15
17
 
16
18
  module NATS
17
19
  class NUID
18
- DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
19
- BASE = 62
20
+ DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".chars
21
+ BASE = 62
20
22
  PREFIX_LENGTH = 12
21
- SEQ_LENGTH = 10
22
- TOTAL_LENGTH = PREFIX_LENGTH + SEQ_LENGTH
23
- MAX_SEQ = BASE**10
24
- MIN_INC = 33
25
- MAX_INC = 333
23
+ SEQ_LENGTH = 10
24
+ TOTAL_LENGTH = PREFIX_LENGTH + SEQ_LENGTH
25
+ MAX_SEQ = BASE**10
26
+ MIN_INC = 33
27
+ MAX_INC = 333
26
28
  INC = MAX_INC - MIN_INC
27
29
 
28
30
  Ractor.make_shareable(DIGITS) if defined?(Ractor)
29
31
 
30
32
  def initialize
31
- @prand = Random.new
32
- @seq = @prand.rand(MAX_SEQ)
33
- @inc = MIN_INC + @prand.rand(INC)
34
- @prefix = ''
33
+ @prand = Random.new
34
+ @seq = @prand.rand(MAX_SEQ)
35
+ @inc = MIN_INC + @prand.rand(INC)
36
+ @prefix = ""
35
37
  randomize_prefix!
36
38
  end
37
39
 
@@ -46,22 +48,31 @@ module NATS
46
48
  # Do this inline 10 times to avoid even more extra allocs,
47
49
  # then use string interpolation of everything which works
48
50
  # faster for doing concat.
49
- s_10 = DIGITS[l % BASE];
51
+ s_10 = DIGITS[l % BASE]
50
52
 
51
53
  # Ugly, but parallel assignment is slightly faster here...
52
- s_09, s_08, s_07, s_06, s_05, s_04, s_03, s_02, s_01 = \
53
- (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]),\
54
- (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]),\
55
- (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE])
54
+ s_09, s_08, s_07, s_06, s_05, s_04, s_03, s_02, s_01 =
55
+ (l /= BASE
56
+ DIGITS[l % BASE]), (l /= BASE
57
+ DIGITS[l % BASE]), (l /= BASE
58
+ DIGITS[l % BASE]),
59
+ (l /= BASE
60
+ DIGITS[l % BASE]), (l /= BASE
61
+ DIGITS[l % BASE]), (l /= BASE
62
+ DIGITS[l % BASE]),
63
+ (l /= BASE
64
+ DIGITS[l % BASE]), (l /= BASE
65
+ DIGITS[l % BASE]), (l /= BASE
66
+ DIGITS[l % BASE])
56
67
  "#{@prefix}#{s_01}#{s_02}#{s_03}#{s_04}#{s_05}#{s_06}#{s_07}#{s_08}#{s_09}#{s_10}"
57
68
  end
58
69
 
59
70
  def randomize_prefix!
60
- @prefix = \
61
- SecureRandom.random_bytes(PREFIX_LENGTH).each_byte
62
- .reduce('') do |prefix, n|
63
- prefix << DIGITS[n % BASE]
64
- end
71
+ @prefix =
72
+ SecureRandom.random_bytes(PREFIX_LENGTH).each_byte
73
+ .reduce("".dup) do |prefix, n|
74
+ prefix << DIGITS[n % BASE]
75
+ end
65
76
  end
66
77
 
67
78
  private
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NATS
4
+ class Service
5
+ class Callbacks
6
+ attr_reader :service, :callbacks
7
+
8
+ def initialize(service)
9
+ @service = service
10
+ @callbacks = {}
11
+ end
12
+
13
+ def register(name, &block)
14
+ callbacks[name] = block
15
+ end
16
+
17
+ def call(name, *args)
18
+ callbacks[name]&.call(*args)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025 The NATS Authors
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ module NATS
17
+ class Service
18
+ class Request < ::NATS::Msg
19
+ attr_reader :error, :endpoint
20
+
21
+ def initialize(opts = {})
22
+ super
23
+ @endpoint = opts[:endpoint]
24
+ @error = nil
25
+ end
26
+
27
+ def respond_with_error(error)
28
+ @error = NATS::Service::ErrorWrapper.new(error)
29
+
30
+ message = dup
31
+ message.subject = reply
32
+ message.reply = ""
33
+ message.data = @error.data
34
+
35
+ message.header = {
36
+ "Nats-Service-Error" => @error.message,
37
+ "Nats-Service-Error-Code" => @error.code
38
+ }
39
+
40
+ respond_msg(message)
41
+ end
42
+
43
+ def inspect
44
+ dot = "..." if @data.length > 10
45
+ dat = "#{data.slice(0, 10)}#{dot}"
46
+ "#<Service::Request(subject: \"#{@subject}\", reply: \"#{@reply}\", data: #{dat.inspect})>"
47
+ end
48
+
49
+ class << self
50
+ def from_msg(svc, msg)
51
+ request = Request.new(endpoint: svc)
52
+ request.subject = msg.subject
53
+ request.reply = msg.reply
54
+ request.data = msg.data
55
+ request.header = msg.header
56
+ request.nc = msg.nc
57
+ request.sub = msg.sub
58
+
59
+ request
60
+ end
61
+ end
62
+ end
63
+
64
+ class Endpoint
65
+ attr_reader :name, :service, :subject, :metadata, :queue, :stats
66
+
67
+ def initialize(name:, options:, parent:, &block)
68
+ validate(name, options)
69
+
70
+ @name = name
71
+
72
+ @service = parent.service
73
+ @subject = build_subject(parent, options)
74
+ @queue = options[:queue] || parent.queue
75
+ @metadata = options[:metadata]
76
+
77
+ @stats = NATS::Service::Stats.new
78
+ @handler = create_handler(block)
79
+
80
+ @stopped = false
81
+ end
82
+
83
+ def stop
84
+ service.client.send(:drain_sub, @handler)
85
+ rescue
86
+ # nothing we can do here
87
+ ensure
88
+ @stopped = true
89
+ end
90
+
91
+ def reset
92
+ stats.reset
93
+ end
94
+
95
+ def stopped?
96
+ @stopped
97
+ end
98
+
99
+ private
100
+
101
+ def validate(name, options)
102
+ Validator.validate(
103
+ name: name,
104
+ subject: options[:subject],
105
+ queue: options[:queue]
106
+ )
107
+ end
108
+
109
+ def build_subject(parent, options)
110
+ subject = options[:subject] || name
111
+
112
+ parent.subject ? "#{parent.subject}.#{subject}" : subject
113
+ end
114
+
115
+ def create_handler(block)
116
+ service.client.subscribe(subject, queue: queue) do |msg|
117
+ started_at = Time.now
118
+
119
+ req = Request.from_msg(self, msg)
120
+ block.call(req)
121
+ stats.error(req.error) if req.error
122
+ rescue NATS::Error => error
123
+ stats.error(error)
124
+ service.stop(error)
125
+
126
+ raise error
127
+ rescue => error
128
+ stats.error(error)
129
+ Request.from_msg(self, msg).respond_with_error(error)
130
+ ensure
131
+ stats.record(started_at)
132
+ end
133
+ rescue => error
134
+ service.stop(error)
135
+ raise error
136
+ end
137
+ end
138
+
139
+ class Endpoints < NATS::Utils::List
140
+ def add(name, options = {}, &block)
141
+ endpoint = Endpoint.new(
142
+ name: name,
143
+ options: options,
144
+ parent: parent,
145
+ &block
146
+ )
147
+
148
+ insert(endpoint)
149
+ parent.service.endpoints.insert(endpoint)
150
+
151
+ endpoint
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NATS
4
+ class Service
5
+ class Error < StandardError; end
6
+
7
+ class InvalidNameError < Error; end
8
+
9
+ class InvalidVersionError < Error; end
10
+
11
+ class InvalidQueueError < Error; end
12
+
13
+ class InvalidSubjectError < Error; end
14
+
15
+ class ErrorWrapper
16
+ attr_reader :code, :message, :data
17
+
18
+ def initialize(error)
19
+ case error
20
+ when Exception
21
+ @code = 500
22
+ @message = error.message
23
+ @data = ""
24
+ when Hash
25
+ @code = error[:code]
26
+ @message = error[:description]
27
+ @data = error[:data]
28
+ when ErrorWrapper
29
+ @code = error.code
30
+ @message = error.message
31
+ @data = error.data
32
+ else
33
+ @code = 500
34
+ @message = error.to_s
35
+ @data = ""
36
+ end
37
+ end
38
+
39
+ def description
40
+ "#{code}:#{message}"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NATS
4
+ class Service
5
+ class Group
6
+ attr_reader :service, :name, :subject, :queue, :groups, :endpoints
7
+
8
+ def initialize(name:, parent:, queue:)
9
+ Validator.validate(name: name, queue: queue)
10
+
11
+ @name = name
12
+
13
+ @service = parent.service
14
+ @subject = parent.subject ? "#{parent.subject}.#{name}" : name
15
+ @queue = queue || parent.queue
16
+
17
+ @groups = Groups.new(self)
18
+ @endpoints = Endpoints.new(self)
19
+ end
20
+ end
21
+
22
+ class Groups < NATS::Utils::List
23
+ def add(name, queue: nil)
24
+ group = Group.new(
25
+ name: name,
26
+ queue: queue,
27
+ parent: parent
28
+ )
29
+
30
+ insert(group)
31
+ parent.service.groups.insert(group)
32
+
33
+ group
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module NATS
6
+ class Service
7
+ class Monitoring
8
+ DEFAULT_PREFIX = "$SRV"
9
+
10
+ VERBS = {
11
+ ping: "PING",
12
+ info: "INFO",
13
+ stats: "STATS"
14
+ }.freeze
15
+
16
+ TYPES = {
17
+ ping: "io.nats.micro.v1.ping_response",
18
+ info: "io.nats.micro.v1.info_response",
19
+ stats: "io.nats.micro.v1.stats_response"
20
+ }.freeze
21
+
22
+ attr_reader :service, :prefix, :stopped
23
+
24
+ def initialize(service, prefix = nil)
25
+ @service = service
26
+ @prefix = prefix || DEFAULT_PREFIX
27
+
28
+ setup_monitors
29
+ end
30
+
31
+ def stop
32
+ return if @monitors.nil?
33
+
34
+ @monitors.each do |monitor|
35
+ service.client.send(:drain_sub, monitor)
36
+ end
37
+ rescue
38
+ # nothing we can do here
39
+ ensure
40
+ @monitors = nil
41
+ end
42
+
43
+ def stopped?
44
+ @monitors.nil?
45
+ end
46
+
47
+ private
48
+
49
+ def setup_monitors
50
+ @monitors = []
51
+
52
+ ping
53
+ info
54
+ stats
55
+ end
56
+
57
+ def ping
58
+ monitor(:ping) do
59
+ {
60
+ type: TYPES[:ping],
61
+ **service.status.basic
62
+ }
63
+ end
64
+ end
65
+
66
+ def info
67
+ monitor(:info) do
68
+ {
69
+ type: TYPES[:info],
70
+ **service.status.info
71
+ }
72
+ end
73
+ end
74
+
75
+ def stats
76
+ monitor(:stats) do
77
+ {
78
+ type: TYPES[:stats],
79
+ **service.status.stats
80
+ }
81
+ end
82
+ end
83
+
84
+ def monitor(verb, &block)
85
+ subjects(verb).map do |subject|
86
+ @monitors << subscribe_monitor(subject, block)
87
+ end
88
+ end
89
+
90
+ def subscribe_monitor(subject, block)
91
+ service.client.subscribe(subject) do |message|
92
+ message.respond(block.call.to_json)
93
+ end
94
+ rescue => error
95
+ service.stop(error)
96
+ raise error
97
+ end
98
+
99
+ def subjects(verb)
100
+ [
101
+ "#{prefix}.#{VERBS[verb]}",
102
+ "#{prefix}.#{VERBS[verb]}.#{service.name}",
103
+ "#{prefix}.#{VERBS[verb]}.#{service.name}.#{service.id}"
104
+ ]
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module NATS
6
+ class Service
7
+ class Stats
8
+ include MonitorMixin
9
+
10
+ attr_reader :num_requests, :num_errors, :last_error, :processing_time, :average_processing_time
11
+
12
+ def initialize
13
+ super
14
+ reset
15
+ end
16
+
17
+ def reset
18
+ synchronize do
19
+ @num_requests = 0
20
+ @processing_time = 0
21
+ @average_processing_time = 0
22
+
23
+ @num_errors = 0
24
+ @last_error = ""
25
+ end
26
+ end
27
+
28
+ def record(started_at)
29
+ synchronize do
30
+ @num_requests += 1
31
+ @processing_time += to_nsec(Time.now - started_at)
32
+ @average_processing_time = @processing_time / @num_requests
33
+ end
34
+ end
35
+
36
+ def error(error)
37
+ error = ErrorWrapper.new(error)
38
+
39
+ synchronize do
40
+ @num_errors += 1
41
+ @last_error = error.description
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def to_nsec(seconds)
48
+ (seconds * 10**9).to_i
49
+ end
50
+ end
51
+ end
52
+ end