diff --git a/src/CorrelationVector.sln b/src/CorrelationVector.sln new file mode 100644 index 0000000..edfa58f --- /dev/null +++ b/src/CorrelationVector.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2027 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CorrelationVector", "Microsoft.CorrelationVector\Microsoft.CorrelationVector.csproj", "{6F211D1F-0E72-4C05-9B9B-4DEC3AF01B38}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CorrelationVector.UnitTests", "Microsoft.CorrelationVector.UnitTests\Microsoft.CorrelationVector.UnitTests.csproj", "{EBE497F1-56EA-4BEA-B834-AEEE261BA6B8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6F211D1F-0E72-4C05-9B9B-4DEC3AF01B38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F211D1F-0E72-4C05-9B9B-4DEC3AF01B38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F211D1F-0E72-4C05-9B9B-4DEC3AF01B38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F211D1F-0E72-4C05-9B9B-4DEC3AF01B38}.Release|Any CPU.Build.0 = Release|Any CPU + {EBE497F1-56EA-4BEA-B834-AEEE261BA6B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBE497F1-56EA-4BEA-B834-AEEE261BA6B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBE497F1-56EA-4BEA-B834-AEEE261BA6B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBE497F1-56EA-4BEA-B834-AEEE261BA6B8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0421635B-CD1D-4001-8D21-FA1A33F7B83E} + EndGlobalSection +EndGlobal diff --git a/src/Microsoft.CorrelationVector.UnitTests/CorrelationVectorTests.cs b/src/Microsoft.CorrelationVector.UnitTests/CorrelationVectorTests.cs new file mode 100644 index 0000000..900cc70 --- /dev/null +++ b/src/Microsoft.CorrelationVector.UnitTests/CorrelationVectorTests.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.CorrelationVector; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CorrelationVector.UnitTests +{ + [TestClass] + public class CorrelationVectorTests + { + [TestMethod] + public void SimpleCreateCorrelationVectorTest() + { + var correlationVector = new CorrelationVector(); + var splitVector = correlationVector.Value.Split('.'); + + Assert.AreEqual(2, splitVector.Length, "Correlation Vector should be created with two components separated by a '.'"); + Assert.AreEqual(16, splitVector[0].Length, "Correlation Vector base should be 16 character long"); + Assert.AreEqual("0", splitVector[1], "Correlation Vector extension should start with zero"); + } + + [TestMethod] + public void CreateV1CorrelationVectorTest() + { + var correlationVector = new CorrelationVector(CorrelationVectorVersion.V1); + var splitVector = correlationVector.Value.Split('.'); + + Assert.AreEqual(2, splitVector.Length, "Correlation Vector should be created with two components separated by a '.'"); + Assert.AreEqual(16, splitVector[0].Length, "Correlation Vector base should be 16 character long"); + Assert.AreEqual("0", splitVector[1], "Correlation Vector extension should start with zero"); + } + + [TestMethod] + public void CreateV2CorrelationVectorTest() + { + var correlationVector = new CorrelationVector(CorrelationVectorVersion.V2); + var splitVector = correlationVector.Value.Split('.'); + + Assert.AreEqual(2, splitVector.Length, "Correlation Vector should be created with two components separated by a '.'"); + Assert.AreEqual(22, splitVector[0].Length, "Correlation Vector base should be 22 character long"); + Assert.AreEqual("0", splitVector[1], "Correlation Vector extension should start with zero"); + } + + [TestMethod] + public void CreateCorrelationVectorFromGuidTest() + { + var guid = System.Guid.NewGuid(); + var correlationVector = new CorrelationVector(guid); + var splitVector = correlationVector.Value.Split('.'); + + Assert.AreEqual(2, splitVector.Length, "Correlation Vector should be created with two components separated by a '.'"); + Assert.AreEqual(22, splitVector[0].Length, "Correlation Vector base should be 22 character long"); + Assert.AreEqual("0", splitVector[1], "Correlation Vector extension should start with zero"); + } + + [TestMethod] + public void ParseCorrelationVectorV1Test() + { + var correlationVector = CorrelationVector.Parse("ifCuqpnwiUimg7Pk.1"); + var splitVector = correlationVector.Value.Split('.'); + + Assert.AreEqual("ifCuqpnwiUimg7Pk", splitVector[0], "Correlation Vector base was not parsed properly"); + Assert.AreEqual("1", splitVector[1], "Correlation Vector extension was not parsed properly"); + } + + [TestMethod] + public void ParseCorrelationVectorV2Test() + { + var correlationVector = CorrelationVector.Parse("Y58xO9ov0kmpPvkiuzMUVA.3.4.5"); + var splitVector = correlationVector.Value.Split('.'); + + Assert.AreEqual(4, splitVector.Length, "Correlation Vector was not parsed properly"); + Assert.AreEqual("Y58xO9ov0kmpPvkiuzMUVA", splitVector[0], "Correlation Vector base was not parsed properly"); + Assert.AreEqual("3", splitVector[1], "Correlation Vector extension was not parsed properly"); + Assert.AreEqual("4", splitVector[2], "Correlation Vector extension was not parsed properly"); + Assert.AreEqual("5", splitVector[3], "Correlation Vector extension was not parsed properly"); + } + + [TestMethod] + public void SimpleIncrementCorrelationVectorTest() + { + var correlationVector = new CorrelationVector(); + correlationVector.Increment(); + var splitVector = correlationVector.Value.Split('.'); + Assert.AreEqual("1", splitVector[1], "Correlation Vector extension should have been incremented by one"); + } + + [TestMethod] + public void SimpleExtendCorrelationVectorTest() + { + var correlationVector = new CorrelationVector(); + var splitVector = correlationVector.Value.Split('.'); + var vectorBase = splitVector[0]; + var extension = splitVector[1]; + + correlationVector = CorrelationVector.Extend(correlationVector.Value); + splitVector = correlationVector.Value.Split('.'); + + Assert.AreEqual(3, splitVector.Length, "Correlation Vector should contain 3 components separated by a '.' after extension"); + Assert.AreEqual(vectorBase, splitVector[0], "Correlation Vector base should contain the same base after extension"); + Assert.AreEqual(extension, splitVector[1], "Correlation Vector should preserve original "); + Assert.AreEqual("0", splitVector[2], "Correlation Vector new extension should start with zero"); + } + + public void ValidateCreationTest() + { + CorrelationVector.ValidateCorrelationVectorDuringCreation = true; + var correlationVector = new CorrelationVector(); + correlationVector.Increment(); + var splitVector = correlationVector.Value.Split('.'); + Assert.AreEqual("1", splitVector[1], "Correlation Vector extension should have been incremented by one"); + } + } +} diff --git a/src/Microsoft.CorrelationVector.UnitTests/Microsoft.CorrelationVector.UnitTests.csproj b/src/Microsoft.CorrelationVector.UnitTests/Microsoft.CorrelationVector.UnitTests.csproj new file mode 100644 index 0000000..75628a8 --- /dev/null +++ b/src/Microsoft.CorrelationVector.UnitTests/Microsoft.CorrelationVector.UnitTests.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + diff --git a/src/Microsoft.CorrelationVector/CorrelationVector.cs b/src/Microsoft.CorrelationVector/CorrelationVector.cs new file mode 100644 index 0000000..28091e9 --- /dev/null +++ b/src/Microsoft.CorrelationVector/CorrelationVector.cs @@ -0,0 +1,350 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Globalization; + +namespace Microsoft.CorrelationVector +{ + /// + /// This class represents a lightweight vector for identifying and measuring + /// causality. + /// + public sealed partial class CorrelationVector : MarshalByRefObject + { + private const byte MaxVectorLength = 63; + private const byte MaxVectorLengthV2 = 127; + private const byte BaseLength = 16; + private const byte BaseLengthV2 = 22; + + private readonly string baseVector = null; + + private int extension = 0; + + private static Random rng = new Random(); + + /// + /// This is the header that should be used between services to pass the correlation + /// vector. + /// + public const string HeaderName = "MS-CV"; + + /// + /// Gets or sets a value indicating whether or not to validate the correlation + /// vector on creation. + /// + public static bool ValidateCorrelationVectorDuringCreation { get; set; } + + /// + /// Creates a new correlation vector by extending an existing value. This should be + /// done at the entry point of an operation. + /// + /// + /// Taken from the message header indicated by . + /// + /// A new correlation vector extended from the current vector. + public static CorrelationVector Extend(string correlationVector) + { + CorrelationVectorVersion version = CorrelationVector.InferVersion( + correlationVector, CorrelationVector.ValidateCorrelationVectorDuringCreation); + + if (CorrelationVector.ValidateCorrelationVectorDuringCreation) + { + CorrelationVector.Validate(correlationVector, version); + } + + return new CorrelationVector(correlationVector, 0, version); + } + + /// + /// Creates a new correlation vector by applying the Spin operator to an existing value. + /// This should be done at the entry point of an operation. + /// + /// + /// Taken from the message header indicated by . + /// + /// A new correlation vector extended from the current vector. + public static CorrelationVector Spin(string correlationVector) + { + SpinParameters defaultParameters = new SpinParameters + { + Interval = SpinCounterInterval.Coarse, + Periodicity = SpinCounterPeriodicity.Short, + Entropy = SpinEntropy.Two + }; + + return CorrelationVector.Spin(correlationVector, defaultParameters); + } + + /// + /// Creates a new correlation vector by applying the Spin operator to an existing value. + /// This should be done at the entry point of an operation. + /// + /// + /// Taken from the message header indicated by . + /// + /// + /// The parameters to use when applying the Spin operator. + /// + /// A new correlation vector extended from the current vector. + public static CorrelationVector Spin(string correlationVector, SpinParameters parameters) + { + CorrelationVectorVersion version = CorrelationVector.InferVersion( + correlationVector, CorrelationVector.ValidateCorrelationVectorDuringCreation); + + if (CorrelationVector.ValidateCorrelationVectorDuringCreation) + { + CorrelationVector.Validate(correlationVector, version); + } + + byte[] entropy = new byte[parameters.EntropyBytes]; + rng.NextBytes(entropy); + + ulong value = (ulong)(DateTime.UtcNow.Ticks >> parameters.TicksBitsToDrop); + for (int i = 0; i < parameters.EntropyBytes; i++) + { + value = (value << 8) | Convert.ToUInt64(entropy[i]); + } + + // Generate a bitmask and mask the lower TotalBits in the value. + // The mask is generated by (1 << TotalBits) - 1. We need to handle the edge case + // when shifting 64 bits, as it wraps around. + value &= (parameters.TotalBits == 64 ? 0 : (ulong)1 << parameters.TotalBits) - 1; + + string s = unchecked((uint)value).ToString(); + if (parameters.TotalBits > 32) + { + s = string.Concat((value >> 32).ToString(), ".", s); + } + + return new CorrelationVector(string.Concat(correlationVector, ".", s), 0, version); + } + + /// + /// Creates a new correlation vector by parsing its string representation + /// + /// correlationVector + /// CorrelationVector + public static CorrelationVector Parse(string correlationVector) + { + if (!string.IsNullOrEmpty(correlationVector)) + { + int p = correlationVector.LastIndexOf('.'); + if (p > 0) + { + int extension; + if (int.TryParse(correlationVector.Substring(p + 1), out extension) && extension >= 0) + { + return new CorrelationVector(correlationVector.Substring(0, p), extension, CorrelationVector.InferVersion(correlationVector, false)); + } + } + } + + return new CorrelationVector(); + } + + /// + /// Initializes a new instance of the class. This + /// should only be called when no correlation vector was found in the message + /// header. + /// + public CorrelationVector() + : this(CorrelationVectorVersion.V1) + { + } + + /// + /// Initializes a new instance of the class of the + /// given implemenation version. This should only be called when no correlation + /// vector was found in the message header. + /// + /// The correlation vector implemenation version. + public CorrelationVector(CorrelationVectorVersion version) + : this(CorrelationVector.GetUniqueValue(version), 0, version) + { + } + + /// + /// Initializes a new instance of the class of the + /// V2 implemenation using the given as the vector base. + /// + /// The to use as a correlation + /// vector base. + public CorrelationVector(Guid vectorBase) + : this(CorrelationVector.GetBaseFromGuid(vectorBase), 0, CorrelationVectorVersion.V2) + { + } + + /// + /// Gets the value of the correlation vector as a string. + /// + public string Value + { + get + { + return string.Concat(this.baseVector, ".", this.extension); + } + } + + /// + /// Increments the current extension by one. Do this before passing the value to an + /// outbound message header. + /// + /// + /// The new value as a string that you can add to the outbound message header + /// indicated by . + /// + public string Increment() + { + int snapshot = 0; + int next = 0; + do + { + snapshot = this.extension; + if (snapshot == int.MaxValue) + { + return this.Value; + } + next = snapshot + 1; + int size = baseVector.Length + 1 + (int)Math.Log10(next) + 1; + if ((this.Version == CorrelationVectorVersion.V1 && + size > CorrelationVector.MaxVectorLength) || + (this.Version == CorrelationVectorVersion.V2 && + size > CorrelationVector.MaxVectorLengthV2)) + { + return this.Value; + } + } + while (snapshot != Interlocked.CompareExchange(ref this.extension, next, snapshot)); + return string.Concat(this.baseVector, ".", next); + } + + /// + /// Gets the version of the correlation vector implementation. + /// + public CorrelationVectorVersion Version + { + get; + private set; + } + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + public override string ToString() + { + return this.Value; + } + + /// + /// Determines whether two instances of the class + /// are equal. + /// + /// + /// The correlation vector you want to compare with the current correlation vector. + /// + /// + /// True if the specified correlation vector is equal to the current correlation + /// vector; otherwise, false. + /// + public bool Equals(CorrelationVector vector) + { + return string.Equals(this.Value, vector.Value, StringComparison.Ordinal); + } + + private CorrelationVector(string baseVector, int extension, CorrelationVectorVersion version) + { + this.baseVector = baseVector; + this.extension = extension; + this.Version = version; + } + + private static string GetBaseFromGuid(Guid guid) + { + byte[] bytes = guid.ToByteArray(); + + // Removes the base64 padding + return Convert.ToBase64String(bytes).Substring(0, CorrelationVector.BaseLengthV2); + } + + private static string GetUniqueValue(CorrelationVectorVersion version) + { + if (CorrelationVectorVersion.V1 == version) + { + byte[] bytes = Guid.NewGuid().ToByteArray(); + return Convert.ToBase64String(bytes, 0, 12); + } + else if (CorrelationVectorVersion.V2 == version) + { + return CorrelationVector.GetBaseFromGuid(Guid.NewGuid()); + } + else + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "Unsupported correlation vector version: {0}", version)); + } + } + + private static CorrelationVectorVersion InferVersion(string correlationVector, bool reportErrors) + { + int index = correlationVector == null ? -1 : correlationVector.IndexOf('.'); + + if (CorrelationVector.BaseLength == index) + { + return CorrelationVectorVersion.V1; + } + else if (CorrelationVector.BaseLengthV2 == index) + { + return CorrelationVectorVersion.V2; + } + else + { + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, "Invalid correlation vector {0}", correlationVector)); + } + } + + private static void Validate(string correlationVector, CorrelationVectorVersion version) + { + byte maxVectorLength; + byte baseLength; + + if (CorrelationVectorVersion.V1 == version) + { + maxVectorLength = CorrelationVector.MaxVectorLength; + baseLength = CorrelationVector.BaseLength; + } + else if (CorrelationVectorVersion.V2 == version) + { + maxVectorLength = CorrelationVector.MaxVectorLengthV2; + baseLength = CorrelationVector.BaseLengthV2; + } + else + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "Unsupported correlation vector version: {0}", version)); + } + + if (string.IsNullOrWhiteSpace(correlationVector) || correlationVector.Length > maxVectorLength) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, + "The {0} correlation vector can not be null or bigger than {1} characters", version, maxVectorLength)); + } + + string[] parts = correlationVector.Split('.'); + + if (parts.Length < 2 || parts[0].Length != baseLength) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid correlation vector {0}. Invalid base value {1}", correlationVector, parts[0])); + } + + for (int i = 1; i < parts.Length; i++) + { + int result; + if (int.TryParse(parts[i], out result) == false || result < 0) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid correlation vector {0}. Invalid extension value {1}", correlationVector, parts[i])); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.CorrelationVector/CorrelationVectorVersion.cs b/src/Microsoft.CorrelationVector/CorrelationVectorVersion.cs new file mode 100644 index 0000000..15f25dd --- /dev/null +++ b/src/Microsoft.CorrelationVector/CorrelationVectorVersion.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.CorrelationVector +{ + public enum CorrelationVectorVersion + { + V1, + V2, + } +} diff --git a/src/Microsoft.CorrelationVector/Microsoft.CorrelationVector.csproj b/src/Microsoft.CorrelationVector/Microsoft.CorrelationVector.csproj new file mode 100644 index 0000000..5766db6 --- /dev/null +++ b/src/Microsoft.CorrelationVector/Microsoft.CorrelationVector.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp2.0 + + + diff --git a/src/Microsoft.CorrelationVector/SpinParameters.cs b/src/Microsoft.CorrelationVector/SpinParameters.cs new file mode 100644 index 0000000..34962ef --- /dev/null +++ b/src/Microsoft.CorrelationVector/SpinParameters.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.CorrelationVector +{ + public enum SpinCounterInterval + { + /// + /// The coarse interval drops the 24 least significant bits in DateTime.Ticks + /// resulting in a counter that increments every 1.67 seconds. + /// + Coarse, + + /// + /// The fine interval drops the 16 least significant bits in DateTime.Ticks + /// resulting in a counter that increments every 6.5 milliseconds. + /// + Fine + } + + public enum SpinCounterPeriodicity + { + /// + /// Do not store a counter as part of the spin value. + /// + None, + + /// + /// The short periodicity stores the counter using 16 bits. + /// + Short, + + /// + /// The medium periodicity stores the counter using 24 bits. + /// + Medium, + + /// + /// The long periodicity stores the counter using 32 bits. + /// + Long + } + + public enum SpinEntropy + { + /// + /// Do not generate entropy as part of the spin value. + /// + None = 0, + + /// + /// Generate entropy using 8 bits. + /// + One = 1, + + /// + /// Generate entropy using 16 bits. + /// + Two = 2, + + /// + /// Generate entropy using 24 bits. + /// + Three = 3, + + /// + /// Generate entropy using 32 bits. + /// + Four = 4 + } + + /// + /// This class stores parameters used by the CorrelationVector Spin operator. + /// + public class SpinParameters : MarshalByRefObject + { + // Internal value for entropy bytes. + private int entropyBytes; + + /// + /// The interval (proportional to time) by which the counter increments. + /// + public SpinCounterInterval Interval { get; set; } + + /// + /// How frequently the counter wraps around to zero, as determined by the amount + /// of space to store the counter. + /// + public SpinCounterPeriodicity Periodicity { get; set; } + + /// + /// The number of bytes to use for entropy. Valid values from a + /// minimum of 0 to a maximum of 4. + /// + public SpinEntropy Entropy + { + get + { + return (SpinEntropy)this.entropyBytes; + } + set + { + this.entropyBytes = (int)value; + } + } + + /// + /// The number of least significant bits to drop in DateTime.Ticks when + /// computing the counter. + /// + internal int TicksBitsToDrop + { + get + { + switch (this.Interval) + { + case SpinCounterInterval.Coarse: + return 24; + + case SpinCounterInterval.Fine: + return 16; + + default: + return 24; + } + } + } + + /// + /// The number of bytes used to store the entropy. + /// + internal int EntropyBytes + { + get + { + return this.entropyBytes; + } + } + + internal int TotalBits + { + get + { + int counterBits; + switch (this.Periodicity) + { + case SpinCounterPeriodicity.None: + counterBits = 0; + break; + case SpinCounterPeriodicity.Short: + counterBits = 16; + break; + case SpinCounterPeriodicity.Medium: + counterBits = 24; + break; + case SpinCounterPeriodicity.Long: + counterBits = 32; + break; + default: + counterBits = 0; + break; + } + + return counterBits + this.EntropyBytes * 8; + } + } + } +}