Skip to content
This repository has been archived by the owner on Sep 3, 2022. It is now read-only.

Commit

Permalink
TimeUuid additional tests and fixes, plus version update
Browse files Browse the repository at this point in the history
  • Loading branch information
reuzel committed Feb 20, 2014
1 parent 2a817c0 commit dae5471
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 63 deletions.
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Version 0.32.0 - Decimal support and Fixes
* Support for decimal types
* TimeGuid generation rewritten in order to guarantee uniqueness when many TimeGuids are generated within a short timeframe.

## Version 0.31.0 - Linq-2-Cql
* Adjustments to support Linq2Cql in CqlSharp.Linq package
* Making ObjectAccessor public accessible
Expand Down
13 changes: 4 additions & 9 deletions CqlSharp/CqlSharp.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,15 @@
See https://github.com/reuzel/CqlSharp/wiki/Features for an extensive feature list.
</description>
<releaseNotes>
Version 0.32.0 - Decimal support and Fixes
* Support for decimal types
* TimeGuid generation rewritten in order to guarantee uniqueness when many TimeGuids are generated within a short timeframe.

Version 0.31.0 - Linq2Cql support
* Changes to support CqlSharp.Linq package
* Making ObjectAccessor public accessible
* Introducing CqlKey and CqlIndex attributes

Version 0.30.2 - Deadlock removal
* Fix: Adding two missing ConfigureAwait statements avoiding deadlock in MVC projects

Version 0.30.1 - Node address fallback
* Fallback to listen-address during discovery when rpc-address is 0.0.0.0. Issue #20
* Performance: Removing some boxing of structs during deserialization

Version 0.30.0 - Binary Protocol V2 support

See https://github.com/reuzel/CqlSharp/blob/master/ChangeLog.md for the full changelog.
</releaseNotes>
<copyright>Copyright 2014 Joost Reuzel</copyright>
Expand Down
4 changes: 2 additions & 2 deletions CqlSharp/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]

[assembly: AssemblyVersion("0.31.0.0")]
[assembly: AssemblyFileVersion("0.31.0.0")]
[assembly: AssemblyVersion("0.32.0.0")]
[assembly: AssemblyFileVersion("0.32.0.0")]

#if DEBUG
[assembly: InternalsVisibleTo("CqlSharp.Fakes")]
Expand Down
115 changes: 85 additions & 30 deletions CqlSharp/TimeGuid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,63 @@
// limitations under the License.

using System;
using System.Threading;

namespace CqlSharp
{
/// <summary>
/// Helper class to generate Time based GUIDs.
/// <remarks>
/// Thanks to <a href="https://github.com/pchalamet/cassandra-sharp">Casssandra-Sharp</a> project.
/// </remarks>
/// </summary>
/// <remarks>
/// The TimeGuid generation does not completely follow the RFC4122 recommendations, to allow
/// for more Guids to be created per 100ns time frame. The used algorithm will increment the
/// clockId when a uuid is requested with the same timestamp as the previous. When the clockId
/// runs out (it has 2^14 possible values), the provided timestamp is incremented by 1, and the
/// clockId is reset.
///
/// When the clock is set backwards, a high chance of collision is there when guids are generated
/// at high speeds. To prevent collisions, not the clockId is incremented (as recommended in
/// RFC4122), but a new nodeId is generated.
/// </remarks>
public static class TimeGuid
{
private static readonly object Lock = new object();
private static readonly object SyncLock = new object();

// multiplex version info
private const int VersionByte = 7;
private const int VersionByteMask = 0x0f;
private const int VersionByteShift = 4;

// offset to move from 1/1/0001, which is 0-time for .NET, to gregorian 0-time of 10/15/1582
private static readonly DateTime GregorianCalendarStart = new DateTime(1582, 10, 15, 0, 0, 0, DateTimeKind.Utc);
public static readonly DateTime GregorianCalendarStart = new DateTime(1582, 10, 15, 0, 0, 0, DateTimeKind.Utc);

private static readonly Random Random = new Random((int)DateTime.UtcNow.Ticks);

// random node that is 16 bytes
private static byte[] _nodeId;

// sequence numbers, etc
private static long _lastTime = 0L;
private static ushort _clockSequenceNumber = 1;
private static int _timeSequenceNumber = 0;
//clockId stuff
private static readonly int ClockSequenceSeed;
private const int MaxClockId = 1 << 14; //14 bits clockId
private static int _clockSequenceNumber;

//time stuff
private static long _lastTime;
private static int _timeSequenceNumber;

/// <summary>
/// Initializes the <see cref="TimeGuid"/> class.
/// </summary>
static TimeGuid()
{
_nodeId = CreateNodeId();
ClockSequenceSeed = Random.Next() % MaxClockId;
_clockSequenceNumber = (ClockSequenceSeed+1) % MaxClockId;
}

/// <summary>
/// Creates a node unique identifier.
/// </summary>
/// <returns></returns>
private static byte[] CreateNodeId()
{
var nodeId = new byte[6];
Expand All @@ -62,6 +82,11 @@ private static byte[] CreateNodeId()
return nodeId;
}

/// <summary>
/// Gets the date time from the provided Guid.
/// </summary>
/// <param name="guid">The unique identifier.</param>
/// <returns></returns>
public static DateTime GetDateTime(this Guid guid)
{
byte[] bytes = guid.ToByteArray();
Expand All @@ -79,14 +104,34 @@ public static DateTime GetDateTime(this Guid guid)
return new DateTime(ticks, DateTimeKind.Utc);
}

public static Guid GenerateTimeBasedGuid(this DateTime dateTime)
/// <summary>
/// Generates a time based unique identifier, set to the current time.
/// </summary>
/// <returns></returns>
public static Guid GenerateTimeBasedGuid()
{
return GenerateTimeBasedGuid(DateTime.UtcNow);
}

/// <summary>
/// Generates a time based unique identifier.
/// </summary>
/// <param name="dateTime">The date time.</param>
/// <param name="node">The node. Must either be null or a 6 byte array. When set to null (recommended), a random node id will be provided</param>
/// <returns></returns>
public static Guid GenerateTimeBasedGuid(this DateTime dateTime, byte[] node = null)
{
if (node != null && node.Length != 6)
throw new ArgumentException("Node must either be null or a byte[] of length 6", "node");

//get the 100ns since calendar start
dateTime = dateTime.ToUniversalTime();
var time = (long)(dateTime - GregorianCalendarStart).TotalMilliseconds;
byte[] nodeId;
var time = (dateTime - GregorianCalendarStart).Ticks;

int sequence;
byte[] nodeId;

lock(Lock)
lock (SyncLock)
{
//generate a new nodeId when clock is set backward (to avoid collisions with earlier created uuids)
if (time < _lastTime)
Expand All @@ -95,38 +140,48 @@ public static Guid GenerateTimeBasedGuid(this DateTime dateTime)
//if time changed, reset sequence numbers
if (time != _lastTime)
{
_clockSequenceNumber = 0;
_clockSequenceNumber = ClockSequenceSeed;
_timeSequenceNumber = 0;
}
else
{
//increment clockSequence, or timeSequence if we are out of clockSequences
if (_clockSequenceNumber == ushort.MaxValue)
{
_clockSequenceNumber = 0;
_timeSequenceNumber = (_timeSequenceNumber + 1) % 10000;
}
else
_clockSequenceNumber++;
//increment time if we are out of clockIds
if (_clockSequenceNumber == ClockSequenceSeed)
_timeSequenceNumber = _timeSequenceNumber + 1;

//increment clockId
_clockSequenceNumber = (_clockSequenceNumber + 1) % MaxClockId;
}

//cache this time
_lastTime = time;

//capture values
time = time * 10000 + _timeSequenceNumber;
time = time + _timeSequenceNumber;
sequence = _clockSequenceNumber;
nodeId = _nodeId;
nodeId = node ?? _nodeId;
}

return GenerateTimeBasedGuid(time, sequence, nodeId);
}

var timeLow = (int)(time);
var timeMid = (short)(time >> 32);
/// <summary>
/// Generates a time based unique identifier.
/// </summary>
/// <param name="time">The time, being the number of 100ns intervals since Gregorian Calendar start</param>
/// <param name="sequence">The sequence, or clockId.</param>
/// <param name="nodeId">The node unique identifier.</param>
/// <returns>a version 1 Guid as documented in RFC4122</returns>
public static Guid GenerateTimeBasedGuid(long time, int sequence, byte[] nodeId)
{
var timeLow = (int)(time & 0xFFFFFFFF);
var timeMid = (short)((time >> 32) & 0xFFFF);
var timeHiAndVersion = (short)(((time >> 48) & 0x0FFF) | (1 << 12));
var seqLow = (byte)(sequence);
var seqLow = (byte)(sequence & 0xFF);
var seqHiAndReserved = (byte)(((sequence & 0x3F00) >> 8) | 0x80);

var guid = new Guid(timeLow, timeMid, timeHiAndVersion, seqLow, seqHiAndReserved, nodeId[0], nodeId[1], nodeId[2], nodeId[3], nodeId[4], nodeId[5]);
var guid = new Guid(timeLow, timeMid, timeHiAndVersion, seqHiAndReserved, seqLow, nodeId[0], nodeId[1], nodeId[2],
nodeId[3], nodeId[4], nodeId[5]);

return guid;
}
Expand Down
1 change: 1 addition & 0 deletions CqlSharpTest/CqlSharp.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
<Compile Include="SerializationTest.cs" />
<Compile Include="SynchronizationContext.cs" />
<Compile Include="ObjectAccessorTest.cs" />
<Compile Include="TimeUuidTest.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CqlSharp\CqlSharp.csproj">
Expand Down
23 changes: 1 addition & 22 deletions CqlSharpTest/IssueTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,28 +60,7 @@ public async Task Issue15()
}
}

[TestMethod]
public void TimeUuidIssue()
{
// this test uses BigInteger to check, otherwise the Dictionary
// will complain because Guid's GetHashCode will collide
var timestamps = new ConcurrentDictionary<BigInteger, Guid>();

Action runner = delegate
{
// run a full clock sequence cycle (or so)
for (var n = 0; n < 10001; n++)
{
var time = DateTime.UtcNow;
var guid = time.GenerateTimeBasedGuid();
var bigint = new BigInteger(guid.ToByteArray());


Assert.IsTrue(timestamps.TryAdd(bigint, guid), "Key already exists!");
//Assert.AreEqual(time.ToTimestamp(), guid.GetDateTime().ToTimestamp());
}
};

Parallel.Invoke(runner, runner, runner, runner);
}
}
}
89 changes: 89 additions & 0 deletions CqlSharpTest/TimeUuidTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// CqlSharp - CqlSharp.Test
// Copyright (c) 2014 Joost Reuzel
//
// 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.Collections.Concurrent;
using System.Numerics;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace CqlSharp.Test
{
[TestClass]
public class TimeUuidTest
{
[TestMethod]
public void TimeUuidIssue()
{
// this test uses BigInteger to check, otherwise the Dictionary
// will complain because Guid's GetHashCode will collide
var timestamps = new ConcurrentDictionary<BigInteger, Guid>();

Action runner = delegate
{
// run a full clock sequence cycle (or so)
for (var n = 0; n < 10001; n++)
{
var time = DateTime.UtcNow;
var guid = time.GenerateTimeBasedGuid();
var bigint = new BigInteger(guid.ToByteArray());

Assert.IsTrue(timestamps.TryAdd(bigint, guid), "Key already exists!");
Assert.AreEqual(time.ToTimestamp(), guid.GetDateTime().ToTimestamp());
}
};

Parallel.Invoke(runner, runner, runner, runner);
}

[TestMethod]
public void TimeUuidRoundTrip()
{
var time = DateTime.UtcNow;
var guid = time.GenerateTimeBasedGuid();
var time2 = guid.GetDateTime();

Assert.AreEqual(time, time2);
}

[TestMethod]
public void ValidateTimeUuidGetDateTime()
{
const string timeUuid = "92ea3200-9a80-11e3-9669-0800200c9a66";
var expected = new DateTime(2014, 2, 20, 22, 44, 36, 256, DateTimeKind.Utc);

var guid = Guid.Parse(timeUuid);
var actual = guid.GetDateTime();

Assert.AreEqual(expected, actual);
}

[TestMethod]
public void ValidateTimeUuidGeneration()
{
const string expected = "92ea3200-9a80-11e3-9669-0800200c9a66";

var dateTime = new DateTime(2014, 2, 20, 22, 44, 36, 256, DateTimeKind.Utc);
var time = (dateTime - TimeGuid.GregorianCalendarStart).Ticks;
var node = new byte[] {0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66};
const int clockId = 5737;

var guid = TimeGuid.GenerateTimeBasedGuid(time, clockId, node);
var actual = guid.ToString("D").ToLower();

Assert.AreEqual(expected, actual);
}
}
}

0 comments on commit dae5471

Please sign in to comment.