Skip to content

Commit

Permalink
Merge pull request #31 from nats-io/nuid
Browse files Browse the repository at this point in the history
Adopt NUID for inboxes generation
  • Loading branch information
wallyqs authored Apr 2, 2018
2 parents 597d0f8 + 4bdccf3 commit d8000d6
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 6 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ gemspec
group :test do
gem 'rake'
gem 'rspec'
gem 'benchmark-ips'
end
4 changes: 3 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
PATH
remote: .
specs:
nats-pure (0.5.0)
nats-pure (0.6.0)

GEM
remote: https://rubygems.org/
specs:
benchmark-ips (2.7.2)
diff-lcs (1.3)
rake (12.3.0)
rspec (3.7.0)
Expand All @@ -26,6 +27,7 @@ PLATFORMS
ruby

DEPENDENCIES
benchmark-ips
nats-pure!
rake
rspec
Expand Down
20 changes: 20 additions & 0 deletions benchmark/nuid_perf.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'securerandom'
require 'nats/nuid'
require 'benchmark/ips'

Benchmark.ips do |x|
x.report "NUID based inboxes with locked instance" do |t|
t.times { "_INBOX.#{NATS::NUID.next}" }
end

x.report "NUID based inboxes with owned instance" do |t|
nuid = NATS::NUID.new
t.times { "_INBOX.#{nuid.next}" }
end

x.report "SecureRandom based inboxes" do |t|
t.times { "_INBOX.#{::SecureRandom.hex(11)}" }
end

x.compare!
end
6 changes: 4 additions & 2 deletions lib/nats/io/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

require 'nats/io/parser'
require 'nats/io/version'
require 'nats/nuid'
require 'thread'
require 'socket'
require 'json'
Expand Down Expand Up @@ -178,6 +179,7 @@ def initialize
@resp_sub = nil
@resp_map = nil
@resp_sub_prefix = nil
@nuid = NATS::NUID.new
end

# Establishes connection to NATS.
Expand Down Expand Up @@ -390,7 +392,7 @@ def request(subject, payload, opts={}, &blk)
start_resp_mux_sub! unless @resp_sub_prefix

# Create token for this request.
token = SecureRandom.hex(11)
token = @nuid.next
inbox = "#{@resp_sub_prefix}.#{token}"

# Create the a future for the request that will
Expand Down Expand Up @@ -1169,7 +1171,7 @@ def start_threads!
# Prepares requests subscription that handles the responses
# for the new style request response.
def start_resp_mux_sub!
@resp_sub_prefix = "_INBOX.#{SecureRandom.hex(11)}"
@resp_sub_prefix = "_INBOX.#{@nuid.next}"
@resp_map = Hash.new { |h,k| h[k] = { }}

@resp_sub = Subscription.new
Expand Down
2 changes: 1 addition & 1 deletion lib/nats/io/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
module NATS
module IO
# NOTE: These are all announced to the server on CONNECT
VERSION = "0.5.0"
VERSION = "0.6.0"
LANG = "#{RUBY_ENGINE}2".freeze
PROTOCOL = 1
end
Expand Down
81 changes: 81 additions & 0 deletions lib/nats/nuid.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright 2016-2018 The NATS Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'securerandom'

module NATS
class NUID
DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
BASE = 62
PREFIX_LENGTH = 12
SEQ_LENGTH = 10
TOTAL_LENGTH = PREFIX_LENGTH + SEQ_LENGTH
MAX_SEQ = BASE**10
MIN_INC = 33
MAX_INC = 333
INC = MAX_INC - MIN_INC

def initialize
@prand = Random.new
@seq = @prand.rand(MAX_SEQ)
@inc = MIN_INC + @prand.rand(INC)
@prefix = ''
randomize_prefix!
end

def next
@seq += @inc
if @seq >= MAX_SEQ
randomize_prefix!
reset_sequential!
end
l = @seq

# Do this inline 10 times to avoid even more extra allocs,
# then use string interpolation of everything which works
# faster for doing concat.
s_10 = DIGITS[l % BASE];

# Ugly, but parallel assignment is slightly faster here...
s_09, s_08, s_07, s_06, s_05, s_04, s_03, s_02, s_01 = \
(l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]),\
(l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]),\
(l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE])
"#{@prefix}#{s_01}#{s_02}#{s_03}#{s_04}#{s_05}#{s_06}#{s_07}#{s_08}#{s_09}#{s_10}"
end

def randomize_prefix!
@prefix = \
SecureRandom.random_bytes(PREFIX_LENGTH).each_byte
.reduce('') do |prefix, n|
prefix << DIGITS[n % BASE]
end
end

private

def reset_sequential!
@seq = @prand.rand(MAX_SEQ)
@inc = MIN_INC + @prand.rand(INC)
end

class << self
@@nuid = NUID.new.extend(MonitorMixin)
def next
@@nuid.synchronize do
@@nuid.next
end
end
end
end
end
5 changes: 3 additions & 2 deletions nats-pure.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,18 @@ spec = Gem::Specification.new do |s|
s.summary = 'NATS is an open-source, high-performance, lightweight cloud messaging system.'
s.homepage = 'https://nats.io'
s.description = 'NATS is an open-source, high-performance, lightweight cloud messaging system.'
s.licenses = ['MIT']
s.licenses = ['Apache-2.0']
s.has_rdoc = false

s.authors = ['Waldemar Quevedo']
s.email = ['wally@apcera.com']
s.email = ['wally@synadia.com']

s.require_paths = ['lib']

s.files = %w[
lib/nats/io/client.rb
lib/nats/io/parser.rb
lib/nats/io/version.rb
lib/nats/nuid.rb
]
end
86 changes: 86 additions & 0 deletions spec/nuid_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright 2016-2018 The NATS Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'spec_helper'

describe 'NUID' do
it "should have a fixed length and be unique" do
nuid = NATS::NUID.new
entries = []
total = 500_000
total.times do
entry = nuid.next
expect(entry.size).to eql(NATS::NUID::TOTAL_LENGTH)
entries << entry
end
entries.uniq!
expect(entries.count).to eql(total)
end

it "should be unique after 1M entries" do
total = 1_000_000
entries = []
nuid = NATS::NUID.new
total.times do
entries << nuid.next
end
entries.uniq!
expect(entries.count).to eql(total)
end

it "should randomize the prefix after sequence is done" do
nuid = NATS::NUID.new
seq_a = nuid.instance_variable_get('@seq')
inc_a = nuid.instance_variable_get('@inc')
a = nuid.next

seq_b = nuid.instance_variable_get('@seq')
inc_b = nuid.instance_variable_get('@inc')
expect(seq_a < seq_b).to eql(true)
expect(seq_b).to eql(seq_a + inc_a)
b = nuid.next

nuid.instance_variable_set('@seq', NATS::NUID::MAX_SEQ+1)
c = nuid.next
l = NATS::NUID::PREFIX_LENGTH
expect(a[0..l]).to eql(b[0..l])
expect(a[0..l]).to_not eql(c[0..l])
end

context "when using the NUID.next" do
it "should be thread safe" do
ts = Hash.new { |h,k| h[k] = { }}
total = 100_000
10.times do |n|
ts[n][:thread] = Thread.new do
sleep 0.01
total.times do
ts[n][:entries] ||= []
ts[n][:entries] << NATS::NUID.next
end
end
end

total_entries = []
ts.each do |k, t|
t[:thread].join
expect(t[:entries].count).to eql(total)
total_entries << t[:entries]
end
total_entries.flatten!
total_entries.uniq!
expect(total_entries.count).to eql(total * 10)
end
end
end
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

$:.unshift('./lib')
require 'nats/io/client'
require 'nats/nuid'
require 'tempfile'
require 'monitor'

Expand Down

0 comments on commit d8000d6

Please sign in to comment.