web-dev-qa-db-ja.com

マルチスレッドエンティティフレームワーク:接続は閉じられませんでした。接続の現在の状態は接続中です

ワークフロープロセスを実行するWindowsサービスプロセスがあります。バックエンドは、Edmxから生成されたエンティティークラスを使用して、Entity Frameworkの上でリポジトリとUnitofWorkパターンとUnityを使用します。必要ではないので、ここでは詳しく説明しませんが、基本的に、ワークフローには5つのステップがあります。特定のプロセスは、任意の時点の任意の段階にある可能性があります(当然のことながら)。ステップ1は、ステップ2のデータを生成するだけで、別のサーバーへの長時間実行プロセスを介してデータを検証します。次に、そのデータを使用してPDFを生成します。各ステージでタイマーを生成しますが、各ステージで複数のタイマーを生成できるように構成できます。そこに問題があります。特定のステージにプロセッサを追加すると、次のエラーがランダムに発生します。

接続は閉じられませんでした。接続の現在の状態は接続中です。

これを読んでみると、コンテキストが2つのスレッドから同じエンティティにアクセスしようとしているため、これが発生していることは明らかです。しかし、これは一種のループを引き起こしているところです。これに関して私が見つけることができるすべての情報は、スレッドごとにインスタンスコンテキストを使用する必要があることを示しています。私の知る限り、これは実行中です(以下のコードを参照)。私はシングルトンパターンやスタティックなどを使用していないので、なぜこれが起こっているのか、またはそれを回避する方法はわかりません。私はあなたのレビューのために以下の私のコードの関連ビットを投稿しました。

基本リポジトリ:

 public class BaseRepository
{
    /// <summary>
    /// Initializes a repository and registers with a <see cref="IUnitOfWork"/>
    /// </summary>
    /// <param name="unitOfWork"></param>
    public BaseRepository(IUnitOfWork unitOfWork)
    {
        if (unitOfWork == null) throw new ArgumentException("unitofWork");
        UnitOfWork = unitOfWork;
    }


    /// <summary>
    /// Returns a <see cref="DbSet"/> of entities.
    /// </summary>
    /// <typeparam name="TEntity">Entity type the dbset needs to return.</typeparam>
    /// <returns></returns>
    protected virtual DbSet<TEntity> GetDbSet<TEntity>() where TEntity : class
    {

        return Context.Set<TEntity>();
    }

    /// <summary>
    /// Sets the state of an entity.
    /// </summary>
    /// <param name="entity">object to set state.</param>
    /// <param name="entityState"><see cref="EntityState"/></param>
    protected virtual void SetEntityState(object entity, EntityState entityState)
    {
        Context.Entry(entity).State = entityState;
    }

    /// <summary>
    /// Unit of work controlling this repository.       
    /// </summary>
    protected IUnitOfWork UnitOfWork { get; set; }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="entity"></param>
    protected virtual void Attach(object entity)
    {
        if (Context.Entry(entity).State == EntityState.Detached)
            Context.Entry(entity).State = EntityState.Modified;
    }

    protected virtual void Detach(object entity)
    {
        Context.Entry(entity).State = EntityState.Detached;
    }

    /// <summary>
    /// Provides access to the ef context we are working with
    /// </summary>
    internal StatementAutoEntities Context
    {
        get
        {                
            return (StatementAutoEntities)UnitOfWork;
        }
    }
}

StatementAutoEntitiesは、自動生成されたEFクラスです。

リポジトリの実装:

public class ProcessingQueueRepository : BaseRepository, IProcessingQueueRepository
{

    /// <summary>
    /// Creates a new repository and associated with a <see cref="IUnitOfWork"/>
    /// </summary>
    /// <param name="unitOfWork"></param>
    public ProcessingQueueRepository(IUnitOfWork unitOfWork) : base(unitOfWork)
    {
    }

    /// <summary>
    /// Create a new <see cref="ProcessingQueue"/> entry in database
    /// </summary>
    /// <param name="Queue">
    ///     <see cref="ProcessingQueue"/>
    /// </param>
    public void Create(ProcessingQueue Queue)
    {
        GetDbSet<ProcessingQueue>().Add(Queue);
        UnitOfWork.SaveChanges();
    }

    /// <summary>
    /// Updates a <see cref="ProcessingQueue"/> entry in database
    /// </summary>
    /// <param name="queue">
    ///     <see cref="ProcessingQueue"/>
    /// </param>
    public void Update(ProcessingQueue queue)
    {
        //Attach(queue);
        UnitOfWork.SaveChanges();
    }

    /// <summary>
    /// Delete a <see cref="ProcessingQueue"/> entry in database
    /// </summary>
    /// <param name="Queue">
    ///     <see cref="ProcessingQueue"/>
    /// </param>
    public void Delete(ProcessingQueue Queue)
    {
        GetDbSet<ProcessingQueue>().Remove(Queue);  
        UnitOfWork.SaveChanges();
    }

    /// <summary>
    /// Gets a <see cref="ProcessingQueue"/> by its unique Id
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public ProcessingQueue GetById(int id)
    {
        return (from e in Context.ProcessingQueue_SelectById(id) select e).FirstOrDefault();
    }

    /// <summary>
    /// Gets a list of <see cref="ProcessingQueue"/> entries by status
    /// </summary>
    /// <param name="status"></param>
    /// <returns></returns>
    public IList<ProcessingQueue> GetByStatus(int status)
    {
        return (from e in Context.ProcessingQueue_SelectByStatus(status) select e).ToList();
    }

    /// <summary>
    /// Gets a list of all <see cref="ProcessingQueue"/> entries
    /// </summary>
    /// <returns></returns>
    public IList<ProcessingQueue> GetAll()
    {
        return (from e in Context.ProcessingQueue_Select() select e).ToList();
    }

    /// <summary>
    /// Gets the next pending item id in the queue for a specific work        
    /// </summary>
    /// <param name="serverId">Unique id of the server that will process the item in the queue</param>
    /// <param name="workTypeId">type of <see cref="WorkType"/> we are looking for</param>
    /// <param name="operationId">if defined only operations of the type indicated are considered.</param>
    /// <returns>Next pending item in the queue for the work type or null if no pending work is found</returns>
    public int GetNextPendingItemId(int serverId, int workTypeId, int? operationId)
    {
        var id = Context.ProcessingQueue_GetNextPending(serverId, workTypeId,  operationId).SingleOrDefault();
        return id.HasValue ? id.Value : -1;
    }

    /// <summary>
    /// Returns a list of <see cref="ProcessingQueueStatus_dto"/>s objects with all
    /// active entries in the queue
    /// </summary>
    /// <returns></returns>
    public IList<ProcessingQueueStatus_dto> GetActiveStatusEntries()
    {
        return (from e in Context.ProcessingQueueStatus_Select() select e).ToList();
    }
    /// <summary>
    /// Bumps an entry to the front of the queue 
    /// </summary>
    /// <param name="processingQueueId"></param>
    public void Bump(int processingQueueId)
    {
        Context.ProcessingQueue_Bump(processingQueueId);
    }
}

依存性注入にUnityを使用します。たとえば、いくつかの呼び出しコード:

#region Members
    private readonly IProcessingQueueRepository _queueRepository;       
    #endregion

    #region Constructors
    /// <summary>Initializes ProcessingQueue services with repositories</summary>
    /// <param name="queueRepository"><see cref="IProcessingQueueRepository"/></param>        
    public ProcessingQueueService(IProcessingQueueRepository queueRepository)
    {
        Check.Require(queueRepository != null, "processingQueueRepository is required");
        _queueRepository = queueRepository;

    }
    #endregion

タイマーを開始するWindowsサービスのコードは次のとおりです。

            _staWorkTypeConfigLock.EnterReadLock();
        foreach (var timer in from operation in (from o in _staWorkTypeConfig.WorkOperations where o.UseQueueForExecution && o.AssignedProcessors > 0 select o) 
                              let interval = operation.SpawnInternval < 30 ? 30 : operation.SpawnInternval 
                              select new StaTimer
                            {
                                Interval = _runImmediate ? 5000 : interval*1000,
                                Operation = (ProcessingQueue.RequestedOperation) operation.OperationId
                            })
        {
            timer.Elapsed += ApxQueueProcessingOnElapsedInterval;
            timer.Enabled = true;
            Logger.DebugFormat("Queue processing for operations of type {0} will execute every {1} seconds", timer.Operation, timer.Interval/1000);                
        }
        _staWorkTypeConfigLock.ExitReadLock();

StaTimerは、タイマーを追加する操作タイプの単なるラッパーです。 ApxQueueProcessingOnElapsedIntervalは、基本的に、操作に基づいてプロセスに作業を割り当てるだけです。

また、タスクを生成する場所にApxQueueProcessingOnElapsedIntervalコードを少し追加します。

            _staTasksLock.EnterWriteLock();
        for (var x = 0; x < tasksNeeded; x++)
        {
            var t = new Task(obj => ProcessStaQueue((QueueProcessConfig) obj),
                             CreateQueueProcessConfig(true, operation), _cancellationToken);


            _staTasks.Add(new Tuple<ProcessingQueue.RequestedOperation, DateTime, Task>(operation, DateTime.Now,t));

            t.Start();
            Thread.Sleep(300); //so there are less conflicts fighting for jobs in the queue table
        }
        _staTasksLock.ExitWriteLock();
24
Brandon

サービス、リポジトリ、コンテキストは、アプリケーションの存続期間中存続するはずですが、それは誤りです。複数のタイマーを同時にトリガーすることができます。つまり、複数のスレッドがサービスを並行して使用し、スレッドでサービスのコードを実行します。コンテキストはスレッドセーフではないため、コンテキストは複数のスレッド間で共有されます=>例外。

唯一のオプションは、実行する操作ごとに新しいコンテキストインスタンスを使用することです。たとえば、クラスを変更して、コンテキストではなくコンテキストファクトリを受け入れ、各操作の新しいコンテキストを取得できます。

27
Ladislav Mrnka

これが誰かを助ける場合:

私の場合、非スレッドセーフDbContextTransientLifetimeが含まれていることを確認しましたが(Ninjectを使用)、それでも並行性の問題が発生していました。一部のカスタムActionFiltersでは、依存関係インジェクションを使用してコンストラクターのDbContextにアクセスしましたが、ActionFiltersには、複数の要求にわたってインスタンス化される存続期間があることがわかりますなので、コンテキストは再作成されませんでした。

コンストラクターではなくOnActionExecutingメソッドの依存関係を手動で解決して、毎回新しいインスタンスになるように修正しました。

7
hofnarwillie