Skip to content

Instantly share code, notes, and snippets.

@RobThree
Last active September 20, 2018 23:10
Show Gist options
  • Save RobThree/8c2e32a1323b7b500d9da11b377b4513 to your computer and use it in GitHub Desktop.
Save RobThree/8c2e32a1323b7b500d9da11b377b4513 to your computer and use it in GitHub Desktop.
Proposed Sequential ULID Rng
using System;
namespace NUlid.Rng
{
/// <summary>
/// An RNG wrapper that uses a configurable increment whenever called within the same millisecond and uses the
/// specified RNG for generating random values for each new timeslot (ms).
/// </summary>
public class SequentialUlidRng : IUlidRng
{
private long _lastts = -1;
private int _incmin = -1;
private int _incmax = -1;
private byte[] _lastvalue;
private IUlidRng _rng;
private object syncRoot = new object();
private static readonly Random _incrementrng = new Random();
public SequentialUlidRng()
: this(new CSUlidRng()) { }
public SequentialUlidRng(IUlidRng rng)
: this(rng, 0, 10000000) { }
public SequentialUlidRng(IUlidRng rng, int increment)
: this(rng, increment, increment) { }
public SequentialUlidRng(int increment)
: this(new CSUlidRng(), increment, increment) { }
public SequentialUlidRng(int incrementmin, int incrementmax)
: this(new CSUlidRng(), incrementmin, incrementmax) { }
public SequentialUlidRng(IUlidRng rng, int incrementmin, int incrementmax)
{
if (rng == null)
throw new ArgumentNullException(nameof(rng));
if (incrementmin <= 0)
throw new ArgumentOutOfRangeException(nameof(incrementmin));
if (incrementmax <= 0)
throw new ArgumentOutOfRangeException(nameof(incrementmax));
if (incrementmin < incrementmax)
throw new ArgumentException("incrementmin must be less than incrementmax");
_rng = rng;
_incmin = incrementmin;
_incmax = incrementmax;
}
public byte[] GetRandomBytes(int length)
{
lock (syncRoot)
{
byte[] value;
var currentts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
// Same millisecond as previous call?
if (currentts == _lastts)
{
// Determine how much we're going to increment this round (either a fixed value or a random amount)
// Note that, when a random increment is used, this increment is *NOT* determined by a
// cryptographically secure RNG. This *does* make increments in the same timeslot, although random,
// a bit more predictable. However, we do get the improved speed of a 'normal' RNG and we can let that
// RNG deal with min/max instead of doing it ourselves and possibly introducing modulo bias etc. Also,
// next timeslot (ms) the 'sequence' will be 'broken' anyway so that mitigates predictability further.
var increment = (uint)(_incmin == _incmax ? _incmin : _incrementrng.Next(_incmin, _incmax));
value = // result of adding increment to _lastvalue;
}
else
{
// New timeslot, generate new random value and keep track of timeslot
value = _rng.GetRandomBytes(length);
_lastts = currentts;
}
_lastvalue = value;
return value;
}
}
byte[] IUlidRng.GetRandomBytes(int length)
{
return this.GetRandomBytes(length);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment