Skip to content

System.NullReferenceException when using Timer in test class #504

@DanielHabenicht

Description

@DanielHabenicht

Hi and sorry for the relatively low effort bug report. (which I think is a bug, in how it is analyzed?)
I think through the way Coyote is executing the test it produces the following error (which never occurs when executing the code normally):

The active test run was aborted. Reason: Test host process crashed : Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.Coyote.Rewriting.Types.Threading.Monitor.SynchronizedBlock.EnterLock()
   at Microsoft.Coyote.Rewriting.Types.Threading.Monitor.SynchronizedBlock.Lock(Object syncObject)
   at Microsoft.Coyote.Rewriting.Types.Threading.Monitor.Enter(Object obj, Boolean& lockTaken)
   at SVN.MacTool.LdapBase.Connect.LdapConnectionPool.CleanupPool(Object state) in C:\Develop\TFS\SVN.MacTool\src\SVN.MacTool.LdapBase\Connect\LdapConnectionPool.cs:line 179
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) 
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) 
   at System.Threading.TimerQueueTimer.Fire(Boolean isThreadPool)
   at System.Threading.TimerQueue.FireNextTimers()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()


Test Run Aborted.

When testing this class

using System.Collections.Concurrent;
using System.DirectoryServices.Protocols;
using System.Net;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using SVN.MacTool.Common.EventBus;
using SVN.MacTool.Common.EventBus.Messages;
using SVN.MacTool.LdapBase.Configuration;

namespace SVN.MacTool.LdapBase.Connect;

public class LdapConnectionPool : ILdapConnectionPool, IConnect
{
    private readonly ConcurrentDictionary<int, LdapConnectionWrapper> _pool = new();

    private readonly Timer _cleanupTimer;

    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    /// <summary>
    /// The configuration
    /// </summary>
    private readonly ILdapConfiguration _configuration;

    /// <summary>
    /// The logger
    /// </summary>
    protected readonly ILogger<LdapConnectionPool> _logger;

    private readonly object _lock = new object();

    public LdapConnectionPool(ILogger<LdapConnectionPool> logger, ILdapConfiguration configuration)
    {
        this._logger = logger;
        this._configuration = configuration;
        this._cleanupTimer = new Timer(this.CleanupPool, null, configuration.ConnectionIdleTimeout, configuration.ConnectionIdleTimeout);
    }

    /// <summary>
    /// Connects to Ldap Server.
    /// </summary>
    /// <returns>LdapConnection.</returns>
    public async Task<LdapConnection> ConnectAsync()
    {
        await this._semaphore.WaitAsync();
        {
            try
            {
                var connection = this.GetConnection();
                if (connection != null)
                {
                    connection.Bind();
                    return connection;

                }
                else
                {
                    this._logger.LogError(
                        $"with Config: {this._configuration.Server} and Port: {this._configuration.Port}");

                    throw new Exception("Keine Verbindung zu Ldap-Server möglich");
                }
            }
            catch (Exception e)
            {

                this._logger.LogError(e.Message, e);
                this._logger.LogError(
                    $"with Config: {this._configuration.Server} and Port: {this._configuration.Port}");
                throw;
            }
            finally
            {
                this._semaphore.Release();
            }
        }
    }

    public LdapConnection GetConnection()
    {
        // Wenn Verbindungen aus dem Pool abrufen werden, müssen wir verhindern,
        // dass die gleiche Verbindung gleichzeitig an mehrere Anforderer ausgegeben wird.
        lock (this._lock)
        {
            var connection = this._pool.FirstOrDefault(p => p.Value.inUse == false);
            if (connection.Value != null)
            {
                connection.Value.inUse = true;
                connection.Value.LastAccessed = DateTime.UtcNow;
                this._logger.LogDebug($"[{Thread.CurrentThread.ManagedThreadId}]: Take connection from pool with ID [{connection.Value.Connection.GetHashCode()}]  ");
                return connection.Value.Connection;
            }

            // Kein Treffer ? Dann neue Verbindung

            var newConnection = this.CreateNewConnection().Connection;
            this._logger.LogDebug($"[{Thread.CurrentThread.ManagedThreadId}]:Create connection with ID [{newConnection.GetHashCode()}]  ");
            return newConnection;
        }
    }

    public void ReturnConnection(LdapConnection connection)
    {
        if (connection == null) throw new ArgumentNullException(nameof(connection));

        this._logger.LogDebug($"[{Thread.CurrentThread.ManagedThreadId}]:Enter ReturnConnection with Connection ID [{connection.GetHashCode()}]  ");


        var key = connection.GetHashCode();

        // LdapConnectionWrapper wrapper = new LdapConnectionWrapper(connection);

        // bool foundConnection = this._pool.TryGetValue(key, out wrapper);

        if (this._pool.TryGetValue(key, out LdapConnectionWrapper wrapper))
        {
            // Mark the connection as not in use if it's found in the pool
            wrapper.inUse = false;
            wrapper.LastAccessed = DateTime.UtcNow;
            this._logger.LogDebug($"Return existing connection to pool with ID {key}.");
        }
        else
        {
            // Only add new connections to the pool if under max size
            if (this._pool.Count < this._configuration.MaxPoolSize)
            {
                var added = this._pool.TryAdd(key, new LdapConnectionWrapper(connection) { inUse = false, LastAccessed = DateTime.UtcNow });
                if (added)
                {
                    this._logger.LogDebug($"Added new connection to pool with ID {key}.");
                }
            }
            else
            {
                // Dispose of the connection if the pool is full
                this._logger.LogDebug($"Pool is full. Disposing connection with ID {key}.");
                connection.Dispose();
            }
        }


    }

    private LdapConnectionWrapper CreateNewConnection()
    {
        lock (this._lock)
        {
            this._logger.LogDebug($"[{Thread.CurrentThread.ManagedThreadId}]:Create new connection ...  ");


            this._logger.LogDebug(
                $"[{Thread.CurrentThread.ManagedThreadId}]:Connecting with SecureBasic to: {this._configuration.Server} and Port: {this._configuration.Port}");

            var ldapIdentifier = new LdapDirectoryIdentifier(this._configuration.Server, this._configuration.Port);
            LdapConnection connection = new LdapConnection(ldapIdentifier)
            {
                AuthType = AuthType.Basic,
                Credential = new NetworkCredential(this._configuration.Username, this._configuration.Password),
                SessionOptions =
                {
                    ProtocolVersion = 3,
                    // Specifies usage of "ldaps://" scheme
                    SecureSocketLayer = true,
                }
            };
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                // TODO: Remove only for development!
                // This option is not working on Linux: https://github.com/dotnet/runtime/issues/60972
                connection.SessionOptions.VerifyServerCertificate = (ldapConnection, certificate) => true;
            }

            return new(connection);
        }
    }

    private void CleanupPool()
    {
        lock (this._lock)
        {
            this._logger.LogDebug($"[{Thread.CurrentThread.ManagedThreadId}]:Try to cleanup connection pool ...  ");


            // Überprüfe und entferne Verbindungen, die länger als _config.ConnectionIdleTimeout ungenutzt sind

            var now = DateTime.UtcNow;
            foreach (var wrapper in this._pool)
            {
                this._logger.LogDebug($"[{Thread.CurrentThread.ManagedThreadId}]:Evaluating Connection with ID [{wrapper.Value.Connection.GetHashCode()}]  ");

                if ((now - wrapper.Value.LastAccessed) > this._configuration.ConnectionIdleTimeout)
                {
                    this._logger.LogDebug($"[{Thread.CurrentThread.ManagedThreadId}]:Cleanup connection with ID [{wrapper.Value.Connection.GetHashCode()}]  ");

                    if (this._pool.TryRemove(wrapper.Value.Connection.GetHashCode(), out var wrapperToRemove))
                    {
                        wrapperToRemove.Connection.Dispose();
                    }
                }
            }
        }
    }

    public void Dispose()
    {
        // this._cleanupTimer.Dispose();
        foreach (var wrapper in this._pool)
        {
            this._logger.LogDebug($"[{Thread.CurrentThread.ManagedThreadId}]:Dispose connection with ID [{wrapper.Value.Connection.GetHashCode()}]  ");
            wrapper.Value.Connection.Dispose();
        }
    }
}

With the following test:

private async Task CoyoteTest_Pool()
    {
        // Arrange
        var ldapConnectionPool = new LdapConnectionPool(this.CreateLogger<LdapConnectionPool>(), new LdapConfiguration(){});


        // Act
        var connection1 = ldapConnectionPool.GetConnection();

        ldapConnectionPool.ReturnConnection(connection1);

        var connection2 = Task.Run(() =>
        {
            return ldapConnectionPool.GetConnection();
        });
        var connection3 = Task.Run(() =>
        {
            return ldapConnectionPool.GetConnection();
        });
        await Task.WhenAll(connection2, connection3);

        // Assert
        connection2.Result.Should().NotBeSameAs(connection3.Result);
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions