Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

System.NullReferenceException when using Timer in test class #504

Open
DanielHabenicht opened this issue Apr 13, 2024 · 0 comments
Open

Comments

@DanielHabenicht
Copy link

DanielHabenicht commented Apr 13, 2024

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);
    }
@DanielHabenicht DanielHabenicht changed the title System.NullReferenceException when using Timer System.NullReferenceException when using Timer in test class Apr 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant