Skip to content

Commit

Permalink
Nuid (#360)
Browse files Browse the repository at this point in the history
NUID Updates
  • Loading branch information
jasper-d authored and ColinSullivan1 committed Jan 28, 2020
1 parent c50d7f6 commit ee6f739
Show file tree
Hide file tree
Showing 16 changed files with 496 additions and 22 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,6 @@ paket-files/
# JetBrains Rider
.idea/
*.sln.iml

# Benchmark artifacts
**/BenchmarkDotNet.Artifacts/
File renamed without changes.
File renamed without changes.
7 changes: 7 additions & 0 deletions src/Benchmarks/MicroBenchmarks/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<!-- We need to override these, otherwise benchmarks won't work :/ -->
<TargetFrameworks></TargetFrameworks>
<TargetFramework></TargetFramework>
</PropertyGroup>
</Project>
25 changes: 25 additions & 0 deletions src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework Condition="$(OS) != 'Windows_NT'">netcoreapp3.1</TargetFramework>
<TargetFrameworks Condition="$(OS) == 'Windows_NT'">netcoreapp3.1;net462</TargetFrameworks>
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>

<PropertyGroup Condition="$(Configuration) == 'Release'">
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\NATS.Client.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\NATS.Client\NATS.Client.csproj" />
</ItemGroup>

</Project>
47 changes: 47 additions & 0 deletions src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2020 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.

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using NATS.Client;
using NATS.Client.Internals;

namespace MicroBenchmarks
{
[DisassemblyDiagnoser(printAsm: true, printSource: true)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[MemoryDiagnoser]
[MarkdownExporterAttribute.GitHub]
[SimpleJob(RuntimeMoniker.Net462)]
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
public class NuidBenchmark
{
#pragma warning disable CS0618
private readonly NUID _nuid = NUID.Instance;
#pragma warning restore CS0618
private readonly Nuid _newNuid = new Nuid(null, 0, 1);

public NuidBenchmark()
{
_nuid.Seq = 0;
}

[BenchmarkCategory("NextNuid")]
[Benchmark(Baseline = true)]
public string NUIDNext() => _nuid.Next;

[BenchmarkCategory("NextNuid"), Benchmark]
public string NextNuid() => _newNuid.GetNext();
}
}
25 changes: 25 additions & 0 deletions src/Benchmarks/MicroBenchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2020 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.

using BenchmarkDotNet.Running;

namespace MicroBenchmarks
{
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<NuidBenchmark>();
}
}
}
21 changes: 3 additions & 18 deletions src/NATS.Client/Conn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public class Connection : IConnection, IDisposable
// .NET 4.0.
private readonly object mu = new Object();

private Random r = null;
private readonly Nuid _nuid = new Nuid();

Options opts = new Options();

Expand Down Expand Up @@ -3191,23 +3191,8 @@ public Task<Msg> RequestAsync(string subject, byte[] data, int offset, int count
/// <returns>A unique inbox string.</returns>
public string NewInbox()
{
var prefix = opts.CustomInboxPrefix ?? IC.inboxPrefix;

if (!opts.UseOldRequestStyle)
{
return prefix + Guid.NewGuid().ToString("N");
}
else
{
if (r == null)
r = new Random(Guid.NewGuid().GetHashCode());

byte[] buf = new byte[13];

r.NextBytes(buf);

return prefix + BitConverter.ToString(buf).Replace("-","");
}
var prefix = opts.customInboxPrefix ?? IC.inboxPrefix;
return prefix + _nuid.GetNext();
}

internal void sendSubscriptionMessage(AsyncSubscription s)
Expand Down
174 changes: 174 additions & 0 deletions src/NATS.Client/Internals/Nuid.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright 2020 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.

using System;
using System.Security.Cryptography;
using System.Threading;

namespace NATS.Client.Internals
{
internal sealed class Nuid
{
private const uint PREFIX_LENGTH = 12;
private const uint SEQUENTIAL_LENGTH = 10;
private const uint NUID_LENGTH = PREFIX_LENGTH + SEQUENTIAL_LENGTH;
private const int MIN_INCREMENT = 33;
private const int MAX_INCREMENT = 333;
private const ulong MAX_SEQUENTIAL = 0x1000_0000_0000_0000; //64^10
private const int BASE = 64;

private static readonly byte[] _digits = new byte[BASE]{
(byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', (byte)'H',
(byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P',
(byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', (byte)'V', (byte)'W', (byte)'X',
(byte)'Y', (byte)'Z', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f',
(byte)'g', (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
(byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', (byte)'v',
(byte)'w', (byte)'x', (byte)'y', (byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3',
(byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_',
};

private readonly object _nuidLock = new object();

private readonly Random _rng;
private readonly RandomNumberGenerator _cryptoRng;

private byte[] _prefix = new byte[PREFIX_LENGTH];
private uint _increment;
private ulong _sequential;

/// <summary>
/// Initializes a new instance of <see cref="Nuid"/>.
/// </summary>
/// <remarks>
/// This constructor is intended to be used from unit tests and
/// benchmarks only. For production use use <see cref="Nuid()"/> instead.
/// </remarks>
/// <param name="rng">A cryptographically strong random number generator.</param>
/// <param name="sequential">The initial sequential.</param>
/// <param name="increment">The initial increment.</param>
internal Nuid(RandomNumberGenerator rng = null, ulong? sequential = null, uint? increment = null)
{
if (rng is null)
_cryptoRng = RandomNumberGenerator.Create();
else
_cryptoRng = rng;

// Instantiating System.Random multiple times in quick succession without a
// proper seed may result in instances that yield identical sequences on .NET FX.
// See https://docs.microsoft.com/en-us/dotnet/api/system.random?view=netframework-4.8#instantiating-the-random-number-generator
// and https://docs.microsoft.com/en-us/dotnet/api/system.random?view=netframework-4.8#avoiding-multiple-instantiations
var seedBytes = new byte[4];
_cryptoRng.GetBytes(seedBytes);
_rng = new Random(BitConverter.ToInt32(seedBytes, 0));

if (sequential is null)
_sequential = GetSequential();
else
_sequential = sequential.Value;

if (increment is null)
_increment = GetIncrement();
else
_increment = increment.Value;

SetPrefix();
}

/// <summary>
/// Initializes a new instance of <see cref="Nuid"/>.
/// </summary>
internal Nuid() : this(null) {}

/// <summary>
/// Returns a random Nuid string.
/// </summary>
/// <remarks>
/// A Nuid is a 132 bit pseudo-random integer encoded as a base64 string
/// </remarks>
/// <returns>The Nuid</returns>
internal string GetNext()
{
var nuidBuffer = new char[NUID_LENGTH];
var sequential = 0UL;

lock (_nuidLock)
{
sequential = _sequential += _increment;
if (_sequential >= MAX_SEQUENTIAL)
{
SetPrefix();
sequential = _sequential = GetSequential();
_increment = GetIncrement();
}

// For small arrays this is way faster than Array.Copy and still faster than Buffer.BlockCopy
for (var i = 0; i < PREFIX_LENGTH; i++)
{
nuidBuffer[i] = (char)_prefix[i];
}
}

for (var i = PREFIX_LENGTH; i < NUID_LENGTH; i++)
{
// We operate on unsigned integers and BASE is a power of two
// therefore we can optimize sequential % BASE to sequential & (BASE - 1)
nuidBuffer[i] = (char)_digits[sequential & (BASE - 1)];
// BASE is 64 = 2^6 and sequential >= 0
// therefore we can optimize sequential / BASE to sequential >> 6
sequential >>= 6;
}

return new string(nuidBuffer);
}

private uint GetIncrement()
{
return (uint)_rng.Next(MIN_INCREMENT, MAX_INCREMENT);
}

private ulong GetSequential()
{
var randomBytes = new byte[8];

_rng.NextBytes(randomBytes);

var sequential = BitConverter.ToUInt64(randomBytes, 0);

// NOTE: Originally we used the following algorithm to create a random long:
// https://stackoverflow.com/a/13095144
// Here, the uRange is const though, because it is always MAX_SEQUENTIAL - 0,
// so the condition can be reduced to:
// sequential > ulong.MaxValue - ((ulong.MaxValue % MAX_SEQUENTIAL) + 1) % MAX_SEQUENTIAL
// The right hand side of the comparision happens to be const too now and can be folded to:
// 18446744073709551615 which happens to be equal to ulong.MaxValue, hence the condition will
// never be true and we can omit the do-while loop entirely.

return sequential % MAX_SEQUENTIAL;
}

private void SetPrefix()
{
var randomBytes = new byte[PREFIX_LENGTH];

_cryptoRng.GetBytes(randomBytes);

for(var i = 0; i < randomBytes.Length; i++)
{
// We operate on unsigned integers and BASE is a power of two
// therefore we can optimize randomBytes[i] % BASE to randomBytes[i] & (BASE - 1)
_prefix[i] = _digits[randomBytes[i] & (BASE - 1)];
}
}
}
}
4 changes: 3 additions & 1 deletion src/NATS.Client/NATS.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<PackageReleaseNotes>https://github.com/nats-io/nats.net/releases</PackageReleaseNotes>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageTags>CNCF NATS Messaging Cloud Publish Subscribe PubSub</PackageTags>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>

<PropertyGroup Condition="$(Configuration) == 'Release'">
Expand All @@ -20,7 +22,7 @@
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\NATS.Client.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
<None Include="package-icon.png">
<Pack>True</Pack>
Expand Down
1 change: 1 addition & 0 deletions src/NATS.Client/NUID.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace NATS.Client
/// that is started at a pseudo random number and increments with a pseudo-random increment.
/// Total is 22 bytes of base 36 ascii text.
/// </summary>
[Obsolete("NATS.Client.NUID is deprecated and will be removed in a future version")]
public class NUID
{
private static char[] digits =
Expand Down
13 changes: 13 additions & 0 deletions src/NATS.Client/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Runtime.CompilerServices;

// Since NATS.Client is signed, friends must be signed too.
// We use the same SNK for simplicity.
// https://docs.microsoft.com/en-us/dotnet/standard/assembly/create-signed-friend
#if DEBUG
[assembly: InternalsVisibleTo("UnitTests")]
[assembly: InternalsVisibleTo("MicroBenchmarks")]

#else
[assembly: InternalsVisibleTo("UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db7da1f2f89089327b47d26d69666fad20861f24e9acdb13965fb6c64dfee8da589b495df37a75e934ddbacb0752a42c40f3dbc79614eec9bb2a0b6741f9e2ad2876f95e74d54c23eef0063eb4efb1e7d824ee8a695b647c113c92834f04a3a83fb60f435814ddf5c4e5f66a168139c4c1b1a50a3e60c164d180e265b1f000cd")]
[assembly: InternalsVisibleTo("MicroBenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db7da1f2f89089327b47d26d69666fad20861f24e9acdb13965fb6c64dfee8da589b495df37a75e934ddbacb0752a42c40f3dbc79614eec9bb2a0b6741f9e2ad2876f95e74d54c23eef0063eb4efb1e7d824ee8a695b647c113c92834f04a3a83fb60f435814ddf5c4e5f66a168139c4c1b1a50a3e60c164d180e265b1f000cd")]
#endif
Loading

0 comments on commit ee6f739

Please sign in to comment.