nats-pure 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
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 -3
  5. data/lib/nats/io/client.rb +303 -280
  6. data/lib/nats/io/errors.rb +2 -0
  7. data/lib/nats/io/jetstream/api.rb +53 -50
  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 +103 -101
  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 +102 -106
  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.4.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