public async Task ExpireClosesTheRemovedItemWhenForced()
        {
            var transportProducer = new ObservableTransportProducerMock();

            var startingPool = new ConcurrentDictionary <string, TransportProducerPool.PoolItem>
            {
                ["0"] = new TransportProducerPool.PoolItem("0", transportProducer),
                ["1"] = new TransportProducerPool.PoolItem("1", transportProducer),
                ["2"] = new TransportProducerPool.PoolItem("2", transportProducer),
            };

            var transportProducerPool = new TransportProducerPool(partition => transportProducer, pool: startingPool, eventHubProducer: transportProducer);

            // Validate the initial state.

            Assert.That(startingPool.TryGetValue("0", out _), Is.True, "The requested partition should appear in the pool.");
            Assert.That(transportProducer.CloseCallCount, Is.EqualTo(0), "The producer should not have been closed.");

            // Request the producer and hold the reference to ensure that it is flagged as being in use.

            await using var poolItem = transportProducerPool.GetPooledProducer("0");

            // Expire the producer and validate the removal state.

            await transportProducerPool.ExpirePooledProducerAsync("0", forceClose : true);

            Assert.That(startingPool.TryGetValue("0", out _), Is.False, "The requested partition should have been removed.");
            Assert.That(transportProducer.CloseCallCount, Is.EqualTo(1), "The producer should have been closed.");
        }
        /// <summary>
        ///   Initializes a new instance of the <see cref="EventHubProducerClient" /> class.
        /// </summary>
        ///
        /// <param name="connection">The <see cref="EventHubConnection" /> connection to use for communication with the Event Hubs service.</param>
        /// <param name="clientOptions">A set of options to apply when configuring the producer.</param>
        ///
        public EventHubProducerClient(EventHubConnection connection,
                                      EventHubProducerClientOptions clientOptions = default)
        {
            Argument.AssertNotNull(connection, nameof(connection));
            clientOptions = clientOptions?.Clone() ?? new EventHubProducerClientOptions();

            OwnsConnection        = false;
            Connection            = connection;
            RetryPolicy           = clientOptions.RetryOptions.ToRetryPolicy();
            PartitionProducerPool = new TransportProducerPool(Connection, RetryPolicy);
        }
        /// <summary>
        ///   Initializes a new instance of the <see cref="EventHubProducerClient" /> class.
        /// </summary>
        ///
        /// <param name="connection">The connection to use as the basis for delegation of client-type operations.</param>
        /// <param name="transportProducer">The transport producer instance to use as the basis for service communication.</param>
        /// <param name="partitionProducerPool">A <see cref="TransportProducerPool" /> used to manage a set of partition specific <see cref="TransportProducer" />.</param>
        ///
        /// <remarks>
        ///   This constructor is intended to be used internally for functional
        ///   testing only.
        /// </remarks>
        ///
        internal EventHubProducerClient(EventHubConnection connection,
                                        TransportProducer transportProducer,
                                        TransportProducerPool partitionProducerPool = default)
        {
            Argument.AssertNotNull(connection, nameof(connection));
            Argument.AssertNotNull(transportProducer, nameof(transportProducer));

            OwnsConnection        = false;
            Connection            = connection;
            RetryPolicy           = new EventHubsRetryOptions().ToRetryPolicy();
            PartitionProducerPool = partitionProducerPool ?? new TransportProducerPool(Connection, RetryPolicy, eventHubProducer: transportProducer);
        }
        /// <summary>
        ///   Initializes a new instance of the <see cref="EventHubProducerClient" /> class.
        /// </summary>
        ///
        /// <param name="connectionString">The connection string to use for connecting to the Event Hubs namespace; it is expected that the shared key properties are contained in this connection string, but not the Event Hub name.</param>
        /// <param name="eventHubName">The name of the specific Event Hub to associate the producer with.</param>
        /// <param name="clientOptions">A set of options to apply when configuring the producer.</param>
        ///
        /// <remarks>
        ///   If the connection string is copied from the Event Hub itself, it will contain the name of the desired Event Hub,
        ///   and can be used directly without passing the <paramref name="eventHubName" />.  The name of the Event Hub should be
        ///   passed only once, either as part of the connection string or separately.
        /// </remarks>
        ///
        /// <seealso href="https://docs.microsoft.com/en-us/azure/event-hubs/event-hubs-get-connection-string"/>
        ///
        public EventHubProducerClient(string connectionString,
                                      string eventHubName,
                                      EventHubProducerClientOptions clientOptions)
        {
            Argument.AssertNotNullOrEmpty(connectionString, nameof(connectionString));
            clientOptions = clientOptions?.Clone() ?? new EventHubProducerClientOptions();

            OwnsConnection        = true;
            Connection            = new EventHubConnection(connectionString, eventHubName, clientOptions.ConnectionOptions);
            RetryPolicy           = clientOptions.RetryOptions.ToRetryPolicy();
            PartitionProducerPool = new TransportProducerPool(Connection, RetryPolicy);
        }
        public void TransportProducerPoolAllowsTakingTheRightTransportProducer(string partitionId)
        {
            var transportProducer = new ObservableTransportProducerMock();
            var partitionProducer = new ObservableTransportProducerMock(partitionId);
            var connection        = new MockConnection(() => partitionProducer);
            var retryPolicy       = new EventHubProducerClientOptions().RetryOptions.ToRetryPolicy();
            var startingPool      = new ConcurrentDictionary <string, TransportProducerPool.PoolItem>
            {
                ["0"] = new TransportProducerPool.PoolItem("0", partitionProducer)
            };
            TransportProducerPool transportProducerPool = new TransportProducerPool(connection, retryPolicy, eventHubProducer: transportProducer);

            var returnedProducer = transportProducerPool.GetPooledProducer(partitionId).TransportProducer as ObservableTransportProducerMock;

            Assert.That(returnedProducer.PartitionId, Is.EqualTo(partitionId));
        }
        /// <summary>
        ///   Initializes a new instance of the <see cref="EventHubProducerClient" /> class.
        /// </summary>
        ///
        /// <param name="fullyQualifiedNamespace">The fully qualified Event Hubs namespace to connect to.  This is likely to be similar to <c>{yournamespace}.servicebus.windows.net</c>.</param>
        /// <param name="eventHubName">The name of the specific Event Hub to associate the producer with.</param>
        /// <param name="credential">The Azure managed identity credential to use for authorization.  Access controls may be specified by the Event Hubs namespace or the requested Event Hub, depending on Azure configuration.</param>
        /// <param name="clientOptions">A set of options to apply when configuring the producer.</param>
        ///
        public EventHubProducerClient(string fullyQualifiedNamespace,
                                      string eventHubName,
                                      TokenCredential credential,
                                      EventHubProducerClientOptions clientOptions = default)
        {
            Argument.AssertWellFormedEventHubsNamespace(fullyQualifiedNamespace, nameof(fullyQualifiedNamespace));
            Argument.AssertNotNullOrEmpty(eventHubName, nameof(eventHubName));
            Argument.AssertNotNull(credential, nameof(credential));

            clientOptions = clientOptions?.Clone() ?? new EventHubProducerClientOptions();

            OwnsConnection        = true;
            Connection            = new EventHubConnection(fullyQualifiedNamespace, eventHubName, credential, clientOptions.ConnectionOptions);
            RetryPolicy           = clientOptions.RetryOptions.ToRetryPolicy();
            PartitionProducerPool = new TransportProducerPool(Connection, RetryPolicy);
        }
        public async Task PoolItemsAreRefreshedOnDisposal()
        {
            var transportProducer = new ObservableTransportProducerMock();
            var startingPool      = new ConcurrentDictionary <string, TransportProducerPool.PoolItem>
            {
                ["0"] = new TransportProducerPool.PoolItem("0", transportProducer)
            };
            var connection  = new MockConnection(() => transportProducer);
            var retryPolicy = new EventHubProducerClientOptions().RetryOptions.ToRetryPolicy();
            TransportProducerPool transportProducerPool = new TransportProducerPool(connection, retryPolicy);
            var expectedTime = DateTimeOffset.UtcNow.AddMinutes(10);

            await using var pooledProducer = transportProducerPool.GetPooledProducer("0");

            // This call should refresh the timespan associated to an item in the pool
            await pooledProducer.DisposeAsync();

            Assert.That(startingPool["0"].RemoveAfter, Is.InRange(expectedTime.AddMinutes(-1), expectedTime.AddMinutes(1)), $"The remove after of a pool item should be extended.");
        }
        public void CloseAsyncSurfacesExceptionsForPartitionTransportProducer()
        {
            var transportProducer = new Mock <TransportProducer>();
            var partitionProducer = new Mock <TransportProducer>();
            var connection        = new MockConnection(() => partitionProducer.Object);
            var retryPolicy       = new EventHubProducerClientOptions().RetryOptions.ToRetryPolicy();
            var startingPool      = new ConcurrentDictionary <string, TransportProducerPool.PoolItem>
            {
                ["0"] = new TransportProducerPool.PoolItem("0", partitionProducer.Object)
            };
            TransportProducerPool transportProducerPool = new TransportProducerPool(connection, retryPolicy, eventHubProducer: transportProducer.Object);

            partitionProducer
            .Setup(producer => producer.CloseAsync(It.IsAny <CancellationToken>()))
            .Returns(Task.FromException(new InvalidCastException()));

            var _ = transportProducerPool.GetPooledProducer("0");

            Assert.That(async() => await transportProducerPool.CloseAsync(), Throws.InstanceOf <InvalidCastException>());
        }
        public void TransportProducerPoolRefreshesAccessedItems()
        {
            DateTimeOffset oneMinuteAgo      = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(1));
            var            transportProducer = new ObservableTransportProducerMock();
            var            connection        = new MockConnection(() => transportProducer);
            var            retryPolicy       = new EventHubProducerClientOptions().RetryOptions.ToRetryPolicy();
            var            startingPool      = new ConcurrentDictionary <string, TransportProducerPool.PoolItem>
            {
                // An expired item in the pool
                ["0"] = new TransportProducerPool.PoolItem("0", transportProducer, removeAfter: oneMinuteAgo)
            };
            TransportProducerPool transportProducerPool = new TransportProducerPool(connection, retryPolicy, startingPool);

            // This call should refresh the timespan associated to the item
            _ = transportProducerPool.GetPooledProducer("0");

            // The expiration call back should not remove the item
            GetExpirationCallBack(transportProducerPool).Invoke(null);

            Assert.That(startingPool.TryGetValue("0", out _), Is.True, "The item in the pool should be refreshed and not have been removed.");
        }
        public async Task TransportProducerPoolAllowsConfiguringRemoveAfter()
        {
            var transportProducer = new ObservableTransportProducerMock();
            var connection        = new MockConnection(() => transportProducer);
            var retryPolicy       = new EventHubProducerClientOptions().RetryOptions.ToRetryPolicy();
            var startingPool      = new ConcurrentDictionary <string, TransportProducerPool.PoolItem>
            {
                ["0"] = new TransportProducerPool.PoolItem("0", transportProducer)
            };
            TransportProducerPool transportProducerPool = new TransportProducerPool(connection, retryPolicy, startingPool);

            var pooledProducer = transportProducerPool.GetPooledProducer("0", TimeSpan.FromMinutes(-1));

            await using (var _ = pooledProducer.ConfigureAwait(false))
            {
            };

            GetExpirationCallBack(transportProducerPool).Invoke(null);

            Assert.That(transportProducer.CloseCallCount, Is.EqualTo(1));
        }
        public void TransportProducerPoolRemovesExpiredItems()
        {
            var            transportProducer = new ObservableTransportProducerMock();
            var            connection        = new MockConnection(() => transportProducer);
            var            retryPolicy       = new EventHubProducerClientOptions().RetryOptions.ToRetryPolicy();
            DateTimeOffset oneMinuteAgo      = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(1));
            var            startingPool      = new ConcurrentDictionary <string, TransportProducerPool.PoolItem>
            {
                // An expired item in the pool
                ["0"] = new TransportProducerPool.PoolItem("0", transportProducer, removeAfter: oneMinuteAgo),
                ["1"] = new TransportProducerPool.PoolItem("0", transportProducer),
                ["2"] = new TransportProducerPool.PoolItem("0", transportProducer),
            };
            TransportProducerPool transportProducerPool = new TransportProducerPool(connection, retryPolicy, startingPool);

            GetExpirationCallBack(transportProducerPool).Invoke(null);

            Assert.That(startingPool.TryGetValue("0", out _), Is.False, "PerformExpiration should remove an expired producer from the pool.");
            Assert.That(transportProducer.CloseCallCount, Is.EqualTo(1), "PerformExpiration should close an expired producer.");
            Assert.That(startingPool.TryGetValue("1", out _), Is.True, "PerformExpiration should not remove valid producers.");
            Assert.That(startingPool.TryGetValue("2", out _), Is.True, "PerformExpiration should not remove valid producers.");
        }
        public async Task ExpireRemovesTheRequestedItem()
        {
            var wasFactoryCalled  = false;
            var transportProducer = new ObservableTransportProducerMock();

            var startingPool = new ConcurrentDictionary <string, TransportProducerPool.PoolItem>
            {
                ["0"] = new TransportProducerPool.PoolItem("0", transportProducer),
                ["1"] = new TransportProducerPool.PoolItem("1", transportProducer),
                ["2"] = new TransportProducerPool.PoolItem("2", transportProducer),
            };

            Func <string, TransportProducer> producerFactory = partition =>
            {
                wasFactoryCalled = true;
                return(transportProducer);
            };

            var transportProducerPool = new TransportProducerPool(producerFactory, pool: startingPool, eventHubProducer: transportProducer);

            // Validate the initial state.

            Assert.That(startingPool.TryGetValue("0", out _), Is.True, "The requested partition should appear in the pool.");
            Assert.That(wasFactoryCalled, Is.False, "No producer should not have been created.");
            Assert.That(transportProducer.CloseCallCount, Is.EqualTo(0), "The producer should not have been closed.");

            // Expire the producer and validate the removal state.

            await transportProducerPool.ExpirePooledProducerAsync("0");

            Assert.That(startingPool.TryGetValue("0", out _), Is.False, "The requested partition should have been removed.");
            Assert.That(transportProducer.CloseCallCount, Is.EqualTo(1), "The producer should have been closed.");
            Assert.That(wasFactoryCalled, Is.False, "The requested partition should not have been created.");

            // Request the producer again and validate a new producer is created.

            Assert.That(transportProducerPool.GetPooledProducer("0"), Is.Not.Null, "The requested partition should be available.");
            Assert.That(wasFactoryCalled, Is.True, "A new producer for the requested partition should have been created.");
        }
        public async Task TransportProducerPoolTracksAProducerUsage()
        {
            DateTimeOffset oneMinuteAgo      = DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMinutes(1));
            var            transportProducer = new ObservableTransportProducerMock();
            var            connection        = new MockConnection(() => transportProducer);
            var            retryPolicy       = new EventHubProducerClientOptions().RetryOptions.ToRetryPolicy();
            var            startingPool      = new ConcurrentDictionary <string, TransportProducerPool.PoolItem>
            {
                // An expired item in the pool
                ["0"] = new TransportProducerPool.PoolItem("0", transportProducer, removeAfter: oneMinuteAgo)
            };
            TransportProducerPool transportProducerPool = new TransportProducerPool(connection, retryPolicy, startingPool);

            var pooledProducer = transportProducerPool.GetPooledProducer("0");

            startingPool.TryGetValue("0", out var poolItem);

            await using (pooledProducer)
            {
                Assert.That(poolItem.ActiveInstances.Count, Is.EqualTo(1), "The usage of a transport producer should be tracked.");
            }

            Assert.That(poolItem.ActiveInstances.Count, Is.EqualTo(0), "After usage an active instance should be removed from the pool.");
        }
 /// <summary>
 ///   Gets the routine responsible of finding expired producers.
 /// </summary>
 ///
 private static TimerCallback GetExpirationCallBack(TransportProducerPool pool) =>
 (TimerCallback)
 typeof(TransportProducerPool)
 .GetMethod("CreateExpirationTimerCallback", BindingFlags.NonPublic | BindingFlags.Instance)
 .Invoke(pool, null);