From 175b72a91b54f60ce7c39e105ef14d43c3eaffdf Mon Sep 17 00:00:00 2001 From: Mayuki Sawatari Date: Fri, 28 Aug 2026 17:52:55 +0900 Subject: [PATCH] Throw when using a disposed looper pool --- src/LogicLooper/LogicLooperPool.cs | 32 +++++++++++++++++--- test/LogicLooper.Test/LogicLooperPoolTest.cs | 28 +++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/LogicLooper/LogicLooperPool.cs b/src/LogicLooper/LogicLooperPool.cs index 446c4e9..0d152ca 100644 --- a/src/LogicLooper/LogicLooperPool.cs +++ b/src/LogicLooper/LogicLooperPool.cs @@ -9,10 +9,17 @@ public sealed partial class LogicLooperPool : ILogicLooperPool, IDisposable { private readonly PooledLogicLooper[] _loopers; private readonly ILogicLooperPoolBalancer _balancer; - private readonly CancellationTokenSource _shutdownTokenSource = new(); + private bool _isDisposed; /// - public IReadOnlyList Loopers => _loopers; + public IReadOnlyList Loopers + { + get + { + ThrowIfDisposed(); + return _loopers; + } + } /// /// Initialize the looper pool with specified configurations. @@ -101,15 +108,25 @@ public Task RegisterActionAsync(LogicLooperAsyncActionWithStateDelegate< /// public async Task ShutdownAsync(TimeSpan shutdownDelay) { + ThrowIfDisposed(); await Task.WhenAll(_loopers.Select(x => x.WrappedLooper.ShutdownAsync(shutdownDelay))); } /// public ILogicLooper GetLooper() - => _balancer.GetPooledLooper(_loopers); + { + ThrowIfDisposed(); + return _balancer.GetPooledLooper(_loopers); + } public void Dispose() { + if (_isDisposed) + { + return; + } + _isDisposed = true; + foreach (var looper in _loopers) { try @@ -120,7 +137,14 @@ public void Dispose() { } } - _shutdownTokenSource.Cancel(); + } + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(LogicLooperPool)); + } } private class PooledLogicLooper : ILogicLooper diff --git a/test/LogicLooper.Test/LogicLooperPoolTest.cs b/test/LogicLooper.Test/LogicLooperPoolTest.cs index d98c3c2..15a58e4 100644 --- a/test/LogicLooper.Test/LogicLooperPoolTest.cs +++ b/test/LogicLooper.Test/LogicLooperPoolTest.cs @@ -141,6 +141,34 @@ public async Task PooledLogicLooper_ShutdownAsyncFromLooperPool() Assert.True(looperFactory.CreatedLoopers[3].IsShutdownRequested); } + [Fact] + public async Task OperationsAfterDispose_ThrowObjectDisposedException() + { + var looperFactory = new FakeLogicLooperPoolLooperFactory(); + var pool = new LogicLooperPool(60, 4, RoundRobinLogicLooperPoolBalancer.Instance, looperFactory); + + pool.Dispose(); + + LogicLooperActionDelegate action = (in LogicLooperActionContext _) => false; + LogicLooperActionWithStateDelegate actionWithState = (in LogicLooperActionContext _, int _) => false; + LogicLooperAsyncActionDelegate asyncAction = _ => ValueTask.FromResult(false); + LogicLooperAsyncActionWithStateDelegate asyncActionWithState = (_, _) => ValueTask.FromResult(false); + + Assert.Throws(() => { _ = pool.RegisterActionAsync(action); }); + Assert.Throws(() => { _ = pool.RegisterActionAsync(action, LooperActionOptions.Default); }); + Assert.Throws(() => { _ = pool.RegisterActionAsync(actionWithState, 0); }); + Assert.Throws(() => { _ = pool.RegisterActionAsync(actionWithState, 0, LooperActionOptions.Default); }); + Assert.Throws(() => { _ = pool.RegisterActionAsync(asyncAction); }); + Assert.Throws(() => { _ = pool.RegisterActionAsync(asyncAction, LooperActionOptions.Default); }); + Assert.Throws(() => { _ = pool.RegisterActionAsync(asyncActionWithState, 0); }); + Assert.Throws(() => { _ = pool.RegisterActionAsync(asyncActionWithState, 0, LooperActionOptions.Default); }); + Assert.Throws(() => pool.GetLooper()); + Assert.Throws(() => _ = pool.Loopers); + await Assert.ThrowsAsync(() => pool.ShutdownAsync(TimeSpan.Zero)); + + pool.Dispose(); + } + class FakeSequentialLogicLooperPoolBalancer : ILogicLooperPoolBalancer { private int _count;