Ejemplo n.º 1
0
        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 + _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 + _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 + _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 + _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);
                    }
                }
            }
        }