Beispiel #1
0
        public void GetBulkhead_WhenInitializingBulkheadAndMaxConcurrentConfigIsInvalid_AndThenConfigChangedToValidValue_CreatesBulkhead()
        {
            // Arrange

            var       key = AnyGroupKey;
            const int invalidMaxConcurrent = -1;
            const int validMaxConcurrent   = 1;

            var mockMetricEvents = new Mock <IMetricEvents>(MockBehavior.Strict);

            var mockConfig = new MjolnirConfiguration
            {
                BulkheadConfigurations = new Dictionary <string, BulkheadConfiguration>
                {
                    {
                        key.Name,
                        new BulkheadConfiguration
                        {
                            MaxConcurrent = invalidMaxConcurrent
                        }
                    }
                }
            };

            var mockLogFactory = new Mock <IMjolnirLogFactory>(MockBehavior.Strict);

            mockLogFactory.Setup(m => m.CreateLog <BulkheadFactory>()).Returns(new DefaultMjolnirLog <BulkheadFactory>());
            mockLogFactory.Setup(m => m.CreateLog <SemaphoreBulkheadHolder>()).Returns(new DefaultMjolnirLog <SemaphoreBulkheadHolder>());
            var factory = new BulkheadFactory(mockMetricEvents.Object, mockConfig, mockLogFactory.Object);

            try
            {
                factory.GetBulkhead(key);
            }
            catch (ArgumentOutOfRangeException)
            {
                // Expected, config is invalid for the first attempt.
            }

            mockConfig.BulkheadConfigurations = new Dictionary <string, BulkheadConfiguration>
            {
                {
                    key.Name,
                    new BulkheadConfiguration
                    {
                        MaxConcurrent = validMaxConcurrent
                    }
                }
            };
            mockConfig.NotifyAfterConfigUpdate();

            // Act

            var bulkhead = factory.GetBulkhead(key); // Should not throw.

            // Assert

            Assert.Equal(validMaxConcurrent, bulkhead.CountAvailable);
        }
Beispiel #2
0
        public void Construct_InitializesConfigGauge_GaugeFiresForOneBulkhead()
        {
            // Arrange

            var key      = AnyString;
            var groupKey = GroupKey.Named(key);
            var expectedMaxConcurrent = AnyPositiveInt;
            var mockMetricEvents      = new Mock <IMetricEvents>(MockBehavior.Strict);

            mockMetricEvents.Setup(m => m.BulkheadGauge(groupKey.Name, "semaphore", expectedMaxConcurrent, It.IsAny <int>()));

            var mockConfig = new MjolnirConfiguration
            {
                BulkheadConfigurations = new Dictionary <string, BulkheadConfiguration>
                {
                    {
                        groupKey.Name,
                        new BulkheadConfiguration
                        {
                            MaxConcurrent = expectedMaxConcurrent
                        }
                    }
                }
            };

            var mockLogFactory = new Mock <IMjolnirLogFactory>(MockBehavior.Strict);

            mockLogFactory.Setup(m => m.CreateLog <BulkheadFactory>()).Returns(new DefaultMjolnirLog <BulkheadFactory>());
            mockLogFactory.Setup(m => m.CreateLog <SemaphoreBulkheadHolder>()).Returns(new DefaultMjolnirLog <SemaphoreBulkheadHolder>());
            // Act

            var factory = new BulkheadFactory(mockMetricEvents.Object, mockConfig, mockLogFactory.Object);

            // Add a bulkhead
            factory.GetBulkhead(groupKey);

            // The timer will fire after 1 second.
            Thread.Sleep(TimeSpan.FromMilliseconds(1500));

            // Assert

            // Gauges should fire every second, so wait one second and then verify.
            mockMetricEvents.Verify(m => m.BulkheadGauge(key, "semaphore", expectedMaxConcurrent, It.IsAny <int>()), Times.AtLeastOnce);
        }
Beispiel #3
0
        public void GetBulkhead_ReturnsSameBulkheadForKey()
        {
            // Bulkheads are long-lived objects and used for many requests. In the absence
            // of any configuration changes, we should be using the same one through the
            // lifetime of the app.

            // Arrange

            var key = AnyGroupKey;

            var mockMetricEvents = new Mock <IMetricEvents>(MockBehavior.Strict);

            var mockConfig = new MjolnirConfiguration
            {
                BulkheadConfigurations = new Dictionary <string, BulkheadConfiguration>
                {
                    {
                        key.Name,
                        new BulkheadConfiguration
                        {
                            MaxConcurrent = AnyPositiveInt
                        }
                    }
                }
            };

            var mockLogFactory = new Mock <IMjolnirLogFactory>(MockBehavior.Strict);

            mockLogFactory.Setup(m => m.CreateLog <BulkheadFactory>()).Returns(new DefaultMjolnirLog <BulkheadFactory>());
            mockLogFactory.Setup(m => m.CreateLog <SemaphoreBulkheadHolder>()).Returns(new DefaultMjolnirLog <SemaphoreBulkheadHolder>());

            var factory = new BulkheadFactory(mockMetricEvents.Object, mockConfig, mockLogFactory.Object);

            // Act

            var bulkhead = factory.GetBulkhead(key);

            // Assert

            Assert.Equal(bulkhead, factory.GetBulkhead(key));
        }
Beispiel #4
0
        public void GetBulkhead_WhenInitializingBulkheadAndMaxConcurrentConfigIsInvalid_Throws()
        {
            // Arrange

            var       key = AnyGroupKey;
            const int invalidMaxConcurrent = -1;

            var mockMetricEvents = new Mock <IMetricEvents>(MockBehavior.Strict);

            var mockConfig = new MjolnirConfiguration
            {
                BulkheadConfigurations = new Dictionary <string, BulkheadConfiguration>
                {
                    {
                        key.Name,
                        new BulkheadConfiguration
                        {
                            MaxConcurrent = invalidMaxConcurrent
                        }
                    }
                }
            };


            var mockLogFactory = new Mock <IMjolnirLogFactory>(MockBehavior.Strict);

            mockLogFactory.Setup(m => m.CreateLog <BulkheadFactory>()).Returns(new DefaultMjolnirLog <BulkheadFactory>());
            mockLogFactory.Setup(m => m.CreateLog <SemaphoreBulkheadHolder>()).Returns(new DefaultMjolnirLog <SemaphoreBulkheadHolder>());
            var factory = new BulkheadFactory(mockMetricEvents.Object, mockConfig, mockLogFactory.Object);

            // Act + Assert

            var exception = Assert.Throws <ArgumentOutOfRangeException>(() => factory.GetBulkhead(key));

            Assert.Equal("maxConcurrent", exception.ParamName);
            Assert.Equal(invalidMaxConcurrent, exception.ActualValue);
        }
Beispiel #5
0
        public void Construct_InitializesConfigGauge_GaugeFiresForMultipleBulkheads()
        {
            // Arrange

            var key1      = AnyString;
            var groupKey1 = GroupKey.Named(key1);

            var key2      = AnyString;
            var groupKey2 = GroupKey.Named(key2);

            var expectedMaxConcurrent1 = AnyPositiveInt;
            var expectedMaxConcurrent2 = AnyPositiveInt;

            var mockMetricEvents = new Mock <IMetricEvents>(MockBehavior.Strict);

            mockMetricEvents.Setup(m => m.BulkheadGauge(groupKey1.Name, "semaphore", expectedMaxConcurrent1, It.IsAny <int>()));
            mockMetricEvents.Setup(m => m.BulkheadGauge(groupKey2.Name, "semaphore", expectedMaxConcurrent2, It.IsAny <int>()));

            var mockConfig = new MjolnirConfiguration
            {
                BulkheadConfigurations = new Dictionary <string, BulkheadConfiguration>
                {
                    {
                        groupKey1.Name,
                        new BulkheadConfiguration
                        {
                            MaxConcurrent = expectedMaxConcurrent1
                        }
                    },
                    {
                        groupKey2.Name,
                        new BulkheadConfiguration
                        {
                            MaxConcurrent = expectedMaxConcurrent2
                        }
                    }
                }
            };

            var mockLogFactory = new Mock <IMjolnirLogFactory>(MockBehavior.Strict);

            mockLogFactory.Setup(m => m.CreateLog <BulkheadFactory>()).Returns(new DefaultMjolnirLog <BulkheadFactory>());
            mockLogFactory.Setup(m => m.CreateLog <SemaphoreBulkheadHolder>()).Returns(new DefaultMjolnirLog <SemaphoreBulkheadHolder>());
            // Act + Assert

            var factory = new BulkheadFactory(mockMetricEvents.Object, mockConfig, mockLogFactory.Object);

            // Wait 2s - since we haven't yet created any bulkheads, we shouldn't have any events.
            Thread.Sleep(TimeSpan.FromMilliseconds(1500));

            mockMetricEvents.Verify(
                m => m.BulkheadGauge(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <int>(), It.IsAny <int>()),
                Times.Never);

            // Add two bulkheads
            factory.GetBulkhead(groupKey1);
            factory.GetBulkhead(groupKey2);

            Thread.Sleep(TimeSpan.FromMilliseconds(1500));

            mockMetricEvents.Verify(m => m.BulkheadGauge(key1, "semaphore", expectedMaxConcurrent1, It.IsAny <int>()), Times.AtLeastOnce);
            mockMetricEvents.Verify(m => m.BulkheadGauge(key2, "semaphore", expectedMaxConcurrent2, It.IsAny <int>()), Times.AtLeastOnce);
        }
Beispiel #6
0
        public void GetBulkhead_ReturnsNewBulkheadWhenConfigChanges()
        {
            // Config can be used to resize the bulkhead at runtime, which results in a new
            // bulkhead being created.

            // To ensure consistency, callers who retrieve a bulkhead and call TryEnter()
            // on it should keep a local reference to the same bulkhead to later call
            // Release() on (rather than re-retrieving the bulkhead from the context).
            // That behavior is tested elsewhere (with the BulkheadInvoker tests).

            // Arrange

            var       key                  = AnyString;
            var       groupKey             = GroupKey.Named(key);
            const int initialExpectedCount = 5;
            const int newExpectedCount     = 6;

            var mockMetricEvents = new Mock <IMetricEvents>(MockBehavior.Strict);

            var mockConfig = new MjolnirConfiguration
            {
                BulkheadConfigurations = new Dictionary <string, BulkheadConfiguration>
                {
                    {
                        groupKey.Name,
                        new BulkheadConfiguration
                        {
                            MaxConcurrent = initialExpectedCount
                        }
                    }
                }
            };


            var mockLogFactory = new Mock <IMjolnirLogFactory>(MockBehavior.Strict);

            mockLogFactory.Setup(m => m.CreateLog <BulkheadFactory>()).Returns(new DefaultMjolnirLog <BulkheadFactory>());
            mockLogFactory.Setup(m => m.CreateLog <SemaphoreBulkheadHolder>()).Returns(new DefaultMjolnirLog <SemaphoreBulkheadHolder>());
            var factory = new BulkheadFactory(mockMetricEvents.Object, mockConfig, mockLogFactory.Object);

            // Act

            var firstBulkhead = factory.GetBulkhead(groupKey);

            mockConfig.BulkheadConfigurations = new Dictionary <string, BulkheadConfiguration>
            {
                {
                    groupKey.Name,
                    new BulkheadConfiguration
                    {
                        MaxConcurrent = newExpectedCount
                    }
                }
            };
            mockConfig.NotifyAfterConfigUpdate();

            // Give the change handler callback enough time to create and reassign the bulkhead.
            Thread.Sleep(500);

            var secondBulkhead = factory.GetBulkhead(groupKey);

            // Assert

            // Shouldn't change any existing referenced bulkheads...
            Assert.Equal(initialExpectedCount, firstBulkhead.CountAvailable);

            // ...but newly-retrieved bulkheads should get a new instance
            // with the updated count.
            Assert.Equal(newExpectedCount, secondBulkhead.CountAvailable);

            // And they shouldn't be the same bulkhead (which should be obvious by this point).
            Assert.False(firstBulkhead == secondBulkhead);
        }
            public void CallsTryEnterAndReleaseOnTheSameBulkheadDuringConfigChange()
            {
                // The assumption tested here is important. If the bulkhead max concurrent
                // configuration value changes, the bulkhead holder will build a new semaphore and
                // swap it out with the old one. However, any bulkheads that acquired a lock on
                // the original semaphore need to release that semaphore instead of the new one.
                // Otherwise, they'll release on the new one. If that happens, the new semaphore's
                // counter won't be accurate respective to the number of commands concurrently
                // executing with it.

                // This test is complicated, but it's one of the most important unit tests in the
                // project. If you need to change it, take care that it gets re-written properly.

                // The test is performed by having the command itself change the config
                // value, which will happen between the TryEnter and Release calls.


                // Arrange

                const int initialMaxConcurrent = 10;
                const int newMaxConcurrent     = 15;

                var key      = AnyString;
                var groupKey = GroupKey.Named(key);


                var mockConfig = new MjolnirConfiguration
                {
                    UseCircuitBreakers     = true,
                    BulkheadConfigurations = new Dictionary <string, BulkheadConfiguration>
                    {
                        {
                            key,
                            new BulkheadConfiguration
                            {
                                MaxConcurrent = initialMaxConcurrent
                            }
                        }
                    }
                };

                var mockBreakerExceptionHandler = new Mock <IBreakerExceptionHandler>(MockBehavior.Strict);

                mockBreakerExceptionHandler.Setup(m => m.IsExceptionIgnored(It.IsAny <Type>())).Returns(false);

                var mockCircuitBreaker = new Mock <ICircuitBreaker>(MockBehavior.Strict);

                mockCircuitBreaker.Setup(m => m.IsAllowing()).Returns(true);
                mockCircuitBreaker.Setup(m => m.Name).Returns(AnyString);
                mockCircuitBreaker.Setup(m => m.Metrics).Returns(new Mock <ICommandMetrics>().Object);
                mockCircuitBreaker.Setup(m => m.MarkSuccess(It.IsAny <long>()));

                var mockCircuitBreakerFactory = new Mock <ICircuitBreakerFactory>(MockBehavior.Strict);

                mockCircuitBreakerFactory.Setup(m => m.GetCircuitBreaker(groupKey)).Returns(mockCircuitBreaker.Object);

                var mockMetricEvents = new Mock <IMetricEvents>(); // Non-Strict: we aren't testing metric events here, let's keep the test simpler.

                var mockLogFactory = new Mock <IMjolnirLogFactory>(MockBehavior.Strict);

                mockLogFactory.Setup(m => m.CreateLog <BulkheadFactory>()).Returns(new Mock <IMjolnirLog <BulkheadFactory> >().Object);
                mockLogFactory.Setup(m => m.CreateLog <SemaphoreBulkheadHolder>()).Returns(new Mock <IMjolnirLog <SemaphoreBulkheadHolder> >().Object);
                // Use a real BulkheadFactory, which will give us access to its BulkheadHolder.
                var bulkheadFactory = new BulkheadFactory(mockMetricEvents.Object, mockConfig, mockLogFactory.Object);
                var holder          = bulkheadFactory.GetBulkheadHolder(groupKey);
                var initialBulkhead = bulkheadFactory.GetBulkhead(groupKey);

                // Use a real BreakerInvoker instead of a mocked one so that we actually
                // invoke the command that changes the config value.
                var breakerInvoker = new BreakerInvoker(mockCircuitBreakerFactory.Object, mockMetricEvents.Object, mockBreakerExceptionHandler.Object);
                var command        = new ChangeBulkheadLimitSyncCommand(key, holder, newMaxConcurrent);

                var invoker = new BulkheadInvoker(breakerInvoker, bulkheadFactory, mockMetricEvents.Object, mockConfig);
                var unusedCancellationToken = CancellationToken.None;

                // Make sure the BulkheadFactory has the expected Bulkhead initialized for the key.
                Assert.Equal(initialMaxConcurrent, bulkheadFactory.GetBulkhead(groupKey).CountAvailable);

                // Act

                var result = invoker.ExecuteWithBulkhead(command, unusedCancellationToken);

                // Assert

                // The assertions here are a bit indirect and, if we were mocking, could be more
                // deterministic. We check to see if the CountAvailable values change correctly.
                // Mocking would let us make Verify calls on TryEnter() and Release(), but mocking
                // is challenging because of how the BulkheadFactory internally keeps hold of the
                // Bulkheads it's managing within SemaphoreBulkheadHolders. The tests here should
                // be okay enough, though.


                // Since the config changed, the factory should have a new bulkhead for the key.
                var newBulkhead = bulkheadFactory.GetBulkhead(groupKey);

                Assert.True(initialBulkhead != newBulkhead);

                // The bulkhead we used should have its original value. We're making sure that
                // we didn't TryEnter() and then skip the Release() because a different bulkhead
                // was used.
                Assert.Equal(initialMaxConcurrent, initialBulkhead.CountAvailable);

                // For the sake of completeness, make sure the config change actually got
                // applied (otherwise we might not be testing an actual config change up
                // above).
                Assert.Equal(newMaxConcurrent, newBulkhead.CountAvailable);
            }