public void TestPercentileAlgorithm_NISTExample()
        {
            PercentileSnapshot p = GetPercentileForValues(951772, 951567, 951937, 951959, 951442, 950610, 951591, 951195, 951772, 950925, 951990, 951682);

            Assert.Equal(951983, p.GetPercentile(90));
            Assert.Equal(951990, p.GetPercentile(100));
        }
        public void TestPercentileAlgorithm_Extremes()
        {
            var p = new PercentileSnapshot(2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 800, 768, 657, 700, 867);

            output.WriteLine("0.01: " + p.GetPercentile(0.01));
            output.WriteLine("10th: " + p.GetPercentile(10));
            output.WriteLine("Median: " + p.GetPercentile(50));
            output.WriteLine("75th: " + p.GetPercentile(75));
            output.WriteLine("90th: " + p.GetPercentile(90));
            output.WriteLine("99th: " + p.GetPercentile(99));
            output.WriteLine("99.5th: " + p.GetPercentile(99.5));
            output.WriteLine("99.99: " + p.GetPercentile(99.99));
            Assert.Equal(2, p.GetPercentile(50));
            Assert.Equal(2, p.GetPercentile(10));
            Assert.Equal(2, p.GetPercentile(75));
            if (p.GetPercentile(95) < 600)
            {
                Assert.True(false, "We expect the 90th to be over 600 to show the extremes but got: " + p.GetPercentile(90));
            }

            if (p.GetPercentile(99) < 600)
            {
                Assert.True(false, "We expect the 99th to be over 600 to show the extremes but got: " + p.GetPercentile(99));
            }
        }
        public void TestPercentileAlgorithm_LowPercentile()
        {
            PercentileSnapshot p = GetPercentileForValues(1, 2);

            Assert.Equal(1, p.GetPercentile(25));
            Assert.Equal(2, p.GetPercentile(75));
        }
        public void TestPercentileAlgorithm_HighPercentile()
        {
            PercentileSnapshot p = GetPercentileForValues(1, 2, 3);

            Assert.Equal(2, p.GetPercentile(50));
            Assert.Equal(3, p.GetPercentile(75));
        }
        public void TestPercentileAlgorithm_Percentiles()
        {
            PercentileSnapshot p = GetPercentileForValues(10, 30, 20, 40);
            Assert.Equal(22, p.GetPercentile(30));
            Assert.Equal(20, p.GetPercentile(25));
            Assert.Equal(40, p.GetPercentile(75));
            Assert.Equal(30, p.GetPercentile(50));

            // invalid percentiles
            Assert.Equal(10, p.GetPercentile(-1));
            Assert.Equal(40, p.GetPercentile(101));
        }
        public void Reset()
        {
            /* no-op if disabled */
            if (!_enabled)
            {
                return;
            }

            // clear buckets so we start over again
            _buckets.Clear();

            // and also make sure the percentile snapshot gets reset
            currentPercentileSnapshot = new PercentileSnapshot(_buckets.Array);
        }
Exemplo n.º 7
0
        /// <summary>
        /// Gets the current bucket. If the time is after the window of the current bucket, a new one will be created.
        /// Internal because it's used in unit tests.
        /// </summary>
        /// <returns>The current bucket.</returns>
        private Bucket GetCurrentBucket()
        {
            long currentTime = this.time.GetCurrentTimeInMillis();

            // Retrieve the latest bucket if the given time is BEFORE the end of the bucket window, otherwise it returns NULL.
            Bucket currentBucket = this.buckets.PeekLast();

            if (currentBucket != null && currentTime < currentBucket.WindowStart + this.BucketSizeInMilliseconds)
            {
                // If we're within the bucket 'window of time' return the current one
                // NOTE: We do not worry if we are BEFORE the window in a weird case of where thread scheduling causes that to occur,
                // we'll just use the latest as long as we're not AFTER the window
                return(currentBucket);
            }

            // If we didn't find the current bucket above, then we have to create one:
            //
            // The following needs to be synchronized/locked even with a synchronized/thread-safe data structure such as LinkedBlockingDeque because
            // the logic involves multiple steps to check existence, create an object then insert the object. The 'check' or 'insertion' themselves
            // are thread-safe by themselves but not the aggregate algorithm, thus we put this entire block of logic inside synchronized.
            //
            // I am using a tryLock if/then (http://download.oracle.com/javase/6/docs/api/java/util/concurrent/locks/Lock.html#tryLock())
            // so that a single thread will get the lock and as soon as one thread gets the lock all others will go the 'else' block
            // and just return the currentBucket until the newBucket is created. This should allow the throughput to be far higher
            // and only slow down 1 thread instead of blocking all of them in each cycle of creating a new bucket based on some testing
            // (and it makes sense that it should as well).
            //
            // This means the timing won't be exact to the millisecond as to what data ends up in a bucket, but that's acceptable.
            // It's not critical to have exact precision to the millisecond, as long as it's rolling, if we can instead reduce the impact synchronization.
            //
            // More importantly though it means that the 'if' block within the lock needs to be careful about what it changes that can still
            // be accessed concurrently in the 'else' block since we're not completely synchronizing access.
            //
            // For example, we can't have a multi-step process to add a bucket, remove a bucket, then update the sum since the 'else' block of code
            // can retrieve the sum while this is all happening. The trade-off is that we don't maintain the rolling sum and let readers just iterate
            // bucket to calculate the sum themselves. This is an example of favoring write-performance instead of read-performance and how the tryLock
            // versus a synchronized block needs to be accommodated.
            if (Monitor.TryEnter(this.newBucketLock))
            {
                try
                {
                    if (this.buckets.PeekLast() == null)
                    {
                        // the list is empty so create the first bucket
                        Bucket newBucket = new Bucket(currentTime, this.bucketDataLength.Get());
                        this.buckets.AddLast(newBucket);
                        return(newBucket);
                    }
                    else
                    {
                        // We go into a loop so that it will create as many buckets as needed to catch up to the current time
                        // as we want the buckets complete even if we don't have transactions during a period of time.
                        for (int i = 0; i < this.NumberOfBuckets; i++)
                        {
                            // We have at least 1 bucket so retrieve it
                            Bucket lastBucket = this.buckets.PeekLast();
                            if (currentTime < lastBucket.WindowStart + this.BucketSizeInMilliseconds)
                            {
                                // If we're within the bucket 'window of time' return the current one
                                // NOTE: We do not worry if we are BEFORE the window in a weird case of where thread scheduling causes that to occur,
                                // we'll just use the latest as long as we're not AFTER the window
                                return(lastBucket);
                            }
                            else if (currentTime - (lastBucket.WindowStart + this.BucketSizeInMilliseconds) > this.TimeInMilliseconds)
                            {
                                // The time passed is greater than the entire rolling counter so we want to clear it all and start from scratch
                                this.Reset();

                                // Recursively call GetCurrentBucket which will create a new bucket and return it.
                                return(this.GetCurrentBucket());
                            }
                            else
                            {
                                // We're past the window so we need to create a new bucket.
                                Bucket[] allBuckets = this.buckets.GetArray();

                                // Create a new bucket and add it as the new 'last' (once this is done other threads will start using it on subsequent retrievals)
                                this.buckets.AddLast(new Bucket(lastBucket.WindowStart + this.BucketSizeInMilliseconds, this.bucketDataLength.Get()));

                                // Add the lastBucket values to the cumulativeSum
                                this.currentPercentileSnapshot = new PercentileSnapshot(allBuckets);
                            }
                        }

                        // We have finished the for-loop and created all of the buckets, so return the lastBucket now.
                        return(this.buckets.PeekLast());
                    }
                }
                finally
                {
                    Monitor.Exit(this.newBucketLock);
                }
            }
            else
            {
                currentBucket = this.buckets.PeekLast();
                if (currentBucket != null)
                {
                    // we didn't get the lock so just return the latest bucket while another thread creates the next one
                    return(currentBucket);
                }
                else
                {
                    // The rare scenario where multiple threads raced to create the very first bucket.
                    // Wait slightly and then use recursion while the other thread finishes creating a bucket.
                    Thread.Sleep(5);
                    return(this.GetCurrentBucket());
                }
            }
        }
            public void TestRolling()
            {
                var dateTimeProviderMock = new Mock <IDateTimeProvider>();
                var currentTime          = new DateTime(2017, 6, 26, 14, 0, 0).Ticks / TimeSpan.TicksPerMillisecond;

                dateTimeProviderMock.Setup(time => time.CurrentTimeInMilliseconds).Returns(currentTime);

                var configurationServiceMock = new Mock <IHystrixConfigurationService>();

                configurationServiceMock.Setup(x => x.GetMetricsRollingPercentileEnabled()).Returns(true);
                HystrixRollingPercentile p = new HystrixRollingPercentile(dateTimeProviderMock.Object, 60000, 12, 1000, configurationServiceMock.Object);

                p.AddValue(1000);
                p.AddValue(1000);
                p.AddValue(1000);
                p.AddValue(2000);

                Assert.Equal(1, p.Buckets.Length);

                // no bucket turnover yet so percentile not yet generated
                Assert.Equal(0, p.GetPercentile(50));

                currentTime += 6000;
                dateTimeProviderMock.Setup(time => time.CurrentTimeInMilliseconds).Returns(currentTime);

                // still only 1 bucket until we touch it again
                Assert.Equal(1, p.Buckets.Length);

                // a bucket has been created so we have a new percentile
                Assert.Equal(1000, p.GetPercentile(50));

                // now 2 buckets since getting a percentile causes bucket retrieval
                Assert.Equal(2, p.Buckets.Length);

                p.AddValue(1000);
                p.AddValue(500);

                // should still be 2 buckets
                Assert.Equal(2, p.Buckets.Length);

                p.AddValue(200);
                p.AddValue(200);
                p.AddValue(1600);
                p.AddValue(200);
                p.AddValue(1600);
                p.AddValue(1600);

                // we haven't progressed to a new bucket so the percentile should be the same and ignore the most recent bucket
                Assert.Equal(1000, p.GetPercentile(50));

                // increment to another bucket so we include all of the above in the PercentileSnapshot
                currentTime += 6000;
                dateTimeProviderMock.Setup(time => time.CurrentTimeInMilliseconds).Returns(currentTime);

                // the rolling version should have the same data as creating a snapshot like this
                PercentileSnapshot ps = new PercentileSnapshot(1000, 1000, 1000, 2000, 1000, 500, 200, 200, 1600, 200, 1600, 1600);

                Assert.Equal(ps.GetPercentile(0.15), p.GetPercentile(0.15));
                Assert.Equal(ps.GetPercentile(0.50), p.GetPercentile(0.50));
                Assert.Equal(ps.GetPercentile(0.90), p.GetPercentile(0.90));
                Assert.Equal(ps.GetPercentile(0.995), p.GetPercentile(0.995));

                // mean = 1000+1000+1000+2000+1000+500+200+200+1600+200+1600+1600/12
                Assert.Equal(991, ps.GetMean());
            }
        private Bucket GetCurrentBucket()
        {
            long currentTime = time.CurrentTimeInMillis;

            /* a shortcut to try and get the most common result of immediately finding the current bucket */

            /**
             * Retrieve the latest bucket if the given time is BEFORE the end of the bucket window, otherwise it returns NULL.
             * NOTE: This is thread-safe because it's accessing 'buckets' which is a LinkedBlockingDeque
             */
            Bucket currentBucket = _buckets.PeekLast;

            if (currentBucket != null && currentTime < currentBucket._windowStart + this._bucketSizeInMilliseconds)
            {
                // if we're within the bucket 'window of time' return the current one
                // NOTE: We do not worry if we are BEFORE the window in a weird case of where thread scheduling causes that to occur,
                // we'll just use the latest as long as we're not AFTER the window
                return(currentBucket);
            }

            /* if we didn't find the current bucket above, then we have to create one */

            /**
             * The following needs to be synchronized/locked even with a synchronized/thread-safe data structure such as LinkedBlockingDeque because
             * the logic involves multiple steps to check existence, create an object then insert the object. The 'check' or 'insertion' themselves
             * are thread-safe by themselves but not the aggregate algorithm, thus we put this entire block of logic inside synchronized.
             * I am using a tryLock if/then (https://download.oracle.com/javase/6/docs/api/java/util/concurrent/locks/Lock.html#tryLock())
             * so that a single thread will get the lock and as soon as one thread gets the lock all others will go the 'else' block
             * and just return the currentBucket until the newBucket is created. This should allow the throughput to be far higher
             * and only slow down 1 thread instead of blocking all of them in each cycle of creating a new bucket based on some testing
             * (and it makes sense that it should as well).
             * This means the timing won't be exact to the millisecond as to what data ends up in a bucket, but that's acceptable.
             * It's not critical to have exact precision to the millisecond, as long as it's rolling, if we can instead reduce the impact synchronization.
             * More importantly though it means that the 'if' block within the lock needs to be careful about what it changes that can still
             * be accessed concurrently in the 'else' block since we're not completely synchronizing access.
             * For example, we can't have a multi-step process to add a bucket, remove a bucket, then update the sum since the 'else' block of code
             * can retrieve the sum while this is all happening. The trade-off is that we don't maintain the rolling sum and let readers just iterate
             * bucket to calculate the sum themselves. This is an example of favoring write-performance instead of read-performance and how the tryLock
             * versus a synchronized block needs to be accommodated.
             */
            bool lockTaken = false;

            Monitor.TryEnter(newBucketLock, ref lockTaken);
            if (lockTaken)
            {
                currentTime = time.CurrentTimeInMillis;
                try
                {
                    if (_buckets.PeekLast == null)
                    {
                        // the list is empty so create the first bucket
                        Bucket newBucket = new Bucket(currentTime, _bucketDataLength);
                        _buckets.AddLast(newBucket);
                        return(newBucket);
                    }
                    else
                    {
                        // We go into a loop so that it will create as many buckets as needed to catch up to the current time
                        // as we want the buckets complete even if we don't have transactions during a period of time.
                        for (int i = 0; i < _numberOfBuckets; i++)
                        {
                            // we have at least 1 bucket so retrieve it
                            Bucket lastBucket = _buckets.PeekLast;
                            if (currentTime < lastBucket._windowStart + this._bucketSizeInMilliseconds)
                            {
                                // if we're within the bucket 'window of time' return the current one
                                // NOTE: We do not worry if we are BEFORE the window in a weird case of where thread scheduling causes that to occur,
                                // we'll just use the latest as long as we're not AFTER the window
                                return(lastBucket);
                            }
                            else if (currentTime - (lastBucket._windowStart + this._bucketSizeInMilliseconds) > _timeInMilliseconds)
                            {
                                // the time passed is greater than the entire rolling counter so we want to clear it all and start from scratch
                                Reset();

                                // recursively call getCurrentBucket which will create a new bucket and return it
                                // return GetCurrentBucket();
                                Bucket newBucket = new Bucket(currentTime, _bucketDataLength);
                                _buckets.AddLast(newBucket);
                                return(newBucket);
                            }
                            else
                            {
                                // we're past the window so we need to create a new bucket
                                Bucket[] allBuckets = _buckets.Array;

                                // create a new bucket and add it as the new 'last' (once this is done other threads will start using it on subsequent retrievals)
                                _buckets.AddLast(new Bucket(lastBucket._windowStart + this._bucketSizeInMilliseconds, _bucketDataLength));

                                // we created a new bucket so let's re-generate the PercentileSnapshot (not including the new bucket)
                                currentPercentileSnapshot = new PercentileSnapshot(allBuckets);
                            }
                        }

                        // we have finished the for-loop and created all of the buckets, so return the lastBucket now
                        return(_buckets.PeekLast);
                    }
                }
                finally
                {
                    Monitor.Exit(newBucketLock);
                }
            }
            else
            {
                currentBucket = _buckets.PeekLast;
                if (currentBucket != null)
                {
                    // we didn't get the lock so just return the latest bucket while another thread creates the next one
                    return(currentBucket);
                }
                else
                {
                    // the rare scenario where multiple threads raced to create the very first bucket
                    // wait slightly and then use recursion while the other thread finishes creating a bucket
                    if (Time.WaitUntil(() => { return(_buckets.PeekLast != null); }, 500))
                    {
                        return(_buckets.PeekLast);
                    }
                    else
                    {
                        return(null);
                    }
                }
            }
        }
Exemplo n.º 10
0
        public void TestRolling()
        {
            var time = new MockedTime();
            var p    = new HystrixRollingPercentile(time, TimeInMilliseconds, NumberOfBuckets, BucketDataLength, Enabled);

            p.AddValue(1000);
            p.AddValue(1000);
            p.AddValue(1000);
            p.AddValue(2000);

            Assert.Equal(1, p._buckets.Size);

            // no bucket turnover yet so percentile not yet generated
            Assert.Equal(0, p.GetPercentile(50));

            time.Increment(6000);

            // still only 1 bucket until we touch it again
            Assert.Equal(1, p._buckets.Size);

            // a bucket has been created so we have a new percentile
            Assert.Equal(1000, p.GetPercentile(50));

            // now 2 buckets since getting a percentile causes bucket retrieval
            Assert.Equal(2, p._buckets.Size);

            p.AddValue(1000);
            p.AddValue(500);

            // should still be 2 buckets
            Assert.Equal(2, p._buckets.Size);

            p.AddValue(200);
            p.AddValue(200);
            p.AddValue(1600);
            p.AddValue(200);
            p.AddValue(1600);
            p.AddValue(1600);

            // we haven't progressed to a new bucket so the percentile should be the same and ignore the most recent bucket
            Assert.Equal(1000, p.GetPercentile(50));

            // Increment to another bucket so we include all of the above in the PercentileSnapshot
            time.Increment(6000);

            // the rolling version should have the same data as creating a snapshot like this
            var ps = new PercentileSnapshot(1000, 1000, 1000, 2000, 1000, 500, 200, 200, 1600, 200, 1600, 1600);

            Assert.Equal(ps.GetPercentile(0.15), p.GetPercentile(0.15));
            Assert.Equal(ps.GetPercentile(0.50), p.GetPercentile(0.50));
            Assert.Equal(ps.GetPercentile(0.90), p.GetPercentile(0.90));
            Assert.Equal(ps.GetPercentile(0.995), p.GetPercentile(0.995));

            output.WriteLine("100th: " + ps.GetPercentile(100) + "  " + p.GetPercentile(100));
            output.WriteLine("99.5th: " + ps.GetPercentile(99.5) + "  " + p.GetPercentile(99.5));
            output.WriteLine("99th: " + ps.GetPercentile(99) + "  " + p.GetPercentile(99));
            output.WriteLine("90th: " + ps.GetPercentile(90) + "  " + p.GetPercentile(90));
            output.WriteLine("50th: " + ps.GetPercentile(50) + "  " + p.GetPercentile(50));
            output.WriteLine("10th: " + ps.GetPercentile(10) + "  " + p.GetPercentile(10));

            // mean = 1000+1000+1000+2000+1000+500+200+200+1600+200+1600+1600/12
            Assert.Equal(991, ps.Mean);
        }
Exemplo n.º 11
0
        public void TestPercentileAlgorithm_Median4()
        {
            var list = new PercentileSnapshot(300, 75, 125, 500, 100, 160, 180, 200, 210, 50, 170);

            Assert.Equal(175, list.GetPercentile(50));
        }
Exemplo n.º 12
0
        public void TestPercentileAlgorithm_Median2()
        {
            var list = new PercentileSnapshot(100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 500);

            Assert.Equal(100, list.GetPercentile(50));
        }
        public void TestPercentileAlgorithm_Median1()
        {
            PercentileSnapshot list = new PercentileSnapshot(100, 100, 100, 100, 200, 200, 200, 300, 300, 300, 300);

            Assert.Equal(200, list.GetPercentile(50));
        }
Exemplo n.º 14
0
        /// <summary>
        /// Gets the current bucket. If the time is after the window of the current bucket, a new one will be created.
        /// Internal because it's used in unit tests.
        /// </summary>
        /// <returns>The current bucket.</returns>
        private Bucket GetCurrentBucket()
        {
            long currentTime = this.time.GetCurrentTimeInMillis();

            // Retrieve the latest bucket if the given time is BEFORE the end of the bucket window, otherwise it returns NULL.
            Bucket currentBucket = this.buckets.PeekLast();
            if (currentBucket != null && currentTime < currentBucket.WindowStart + this.BucketSizeInMilliseconds)
            {
                // If we're within the bucket 'window of time' return the current one
                // NOTE: We do not worry if we are BEFORE the window in a weird case of where thread scheduling causes that to occur,
                // we'll just use the latest as long as we're not AFTER the window
                return currentBucket;
            }

            // If we didn't find the current bucket above, then we have to create one:
            //
            // The following needs to be synchronized/locked even with a synchronized/thread-safe data structure such as LinkedBlockingDeque because
            // the logic involves multiple steps to check existence, create an object then insert the object. The 'check' or 'insertion' themselves
            // are thread-safe by themselves but not the aggregate algorithm, thus we put this entire block of logic inside synchronized.
            // 
            // I am using a tryLock if/then (http://download.oracle.com/javase/6/docs/api/java/util/concurrent/locks/Lock.html#tryLock())
            // so that a single thread will get the lock and as soon as one thread gets the lock all others will go the 'else' block
            // and just return the currentBucket until the newBucket is created. This should allow the throughput to be far higher
            // and only slow down 1 thread instead of blocking all of them in each cycle of creating a new bucket based on some testing
            // (and it makes sense that it should as well).
            // 
            // This means the timing won't be exact to the millisecond as to what data ends up in a bucket, but that's acceptable.
            // It's not critical to have exact precision to the millisecond, as long as it's rolling, if we can instead reduce the impact synchronization.
            // 
            // More importantly though it means that the 'if' block within the lock needs to be careful about what it changes that can still
            // be accessed concurrently in the 'else' block since we're not completely synchronizing access.
            // 
            // For example, we can't have a multi-step process to add a bucket, remove a bucket, then update the sum since the 'else' block of code
            // can retrieve the sum while this is all happening. The trade-off is that we don't maintain the rolling sum and let readers just iterate
            // bucket to calculate the sum themselves. This is an example of favoring write-performance instead of read-performance and how the tryLock
            // versus a synchronized block needs to be accommodated.
            if (Monitor.TryEnter(this.newBucketLock))
            {
                try
                {
                    if (this.buckets.PeekLast() == null)
                    {
                        // the list is empty so create the first bucket
                        Bucket newBucket = new Bucket(currentTime, this.bucketDataLength.Get());
                        this.buckets.AddLast(newBucket);
                        return newBucket;
                    }
                    else
                    {
                        // We go into a loop so that it will create as many buckets as needed to catch up to the current time
                        // as we want the buckets complete even if we don't have transactions during a period of time.
                        for (int i = 0; i < this.NumberOfBuckets; i++)
                        {
                            // We have at least 1 bucket so retrieve it
                            Bucket lastBucket = this.buckets.PeekLast();
                            if (currentTime < lastBucket.WindowStart + this.BucketSizeInMilliseconds)
                            {
                                // If we're within the bucket 'window of time' return the current one
                                // NOTE: We do not worry if we are BEFORE the window in a weird case of where thread scheduling causes that to occur,
                                // we'll just use the latest as long as we're not AFTER the window
                                return lastBucket;
                            }
                            else if (currentTime - (lastBucket.WindowStart + this.BucketSizeInMilliseconds) > this.TimeInMilliseconds)
                            {
                                // The time passed is greater than the entire rolling counter so we want to clear it all and start from scratch
                                this.Reset();

                                // Recursively call GetCurrentBucket which will create a new bucket and return it.
                                return this.GetCurrentBucket();
                            }
                            else
                            {
                                // We're past the window so we need to create a new bucket.
                                Bucket[] allBuckets = this.buckets.GetArray();

                                // Create a new bucket and add it as the new 'last' (once this is done other threads will start using it on subsequent retrievals)
                                this.buckets.AddLast(new Bucket(lastBucket.WindowStart + this.BucketSizeInMilliseconds, this.bucketDataLength.Get()));

                                // Add the lastBucket values to the cumulativeSum
                                this.currentPercentileSnapshot = new PercentileSnapshot(allBuckets);
                            }
                        }

                        // We have finished the for-loop and created all of the buckets, so return the lastBucket now.
                        return this.buckets.PeekLast();
                    }
                }
                finally
                {
                    Monitor.Exit(this.newBucketLock);
                }
            }
            else
            {
                currentBucket = this.buckets.PeekLast();
                if (currentBucket != null)
                {
                    // we didn't get the lock so just return the latest bucket while another thread creates the next one
                    return currentBucket;
                }
                else
                {
                    // The rare scenario where multiple threads raced to create the very first bucket.
                    // Wait slightly and then use recursion while the other thread finishes creating a bucket.
                    Thread.Sleep(5);
                    return this.GetCurrentBucket();
                }
            }
        }