OSDN Git Service

AsyncExceptionBoundaryクラスを追加
authorKimura Youichi <kim.upsilon@bucyou.net>
Sat, 6 Jan 2024 04:26:55 +0000 (13:26 +0900)
committerKimura Youichi <kim.upsilon@bucyou.net>
Sat, 6 Jan 2024 04:50:53 +0000 (13:50 +0900)
OpenTween.Tests/AsyncExceptionBoundaryTest.cs [new file with mode: 0644]
OpenTween/AsyncExceptionBoundary.cs [new file with mode: 0644]
OpenTween/ImageCache.cs
OpenTween/TaskCollection.cs
OpenTween/TweetThumbnail.cs

diff --git a/OpenTween.Tests/AsyncExceptionBoundaryTest.cs b/OpenTween.Tests/AsyncExceptionBoundaryTest.cs
new file mode 100644 (file)
index 0000000..f17e34b
--- /dev/null
@@ -0,0 +1,128 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2024 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace OpenTween
+{
+    public class AsyncExceptionBoundaryTest
+    {
+        [Fact]
+        public async Task Wrap_SynchronousTest()
+        {
+            static Task AsyncFunc()
+                => throw new OperationCanceledException();
+
+            // 例外を無視して終了する
+            await AsyncExceptionBoundary.Wrap(AsyncFunc);
+        }
+
+        [Fact]
+        public async Task Wrap_AsynchronousTest()
+        {
+            static async Task AsyncFunc()
+            {
+                await Task.Yield();
+                throw new OperationCanceledException();
+            }
+
+            // 例外を無視して終了する
+            await AsyncExceptionBoundary.Wrap(AsyncFunc);
+        }
+
+        [Fact]
+        public async Task Wrap_IgnoredTest()
+        {
+            static Task AsyncFunc()
+                => throw new OperationCanceledException();
+
+            static bool IgnoreException(Exception ex)
+                => ex is OperationCanceledException;
+
+            // 例外を無視して終了する
+            await AsyncExceptionBoundary.Wrap(AsyncFunc, IgnoreException);
+        }
+
+        [Fact]
+        public async Task Wrap_NotIgnoredTest()
+        {
+            static Task AsyncFunc()
+                => throw new IOException();
+
+            static bool IgnoreException(Exception ex)
+                => ex is OperationCanceledException;
+
+            // 例外を返して終了する
+            await Assert.ThrowsAsync<IOException>(
+                () => AsyncExceptionBoundary.Wrap(AsyncFunc, IgnoreException)
+            );
+        }
+
+        [Fact]
+        public async Task IgnoreException_Test()
+        {
+            var task = Task.FromException(new OperationCanceledException());
+
+            // 例外を無視して終了する
+            await AsyncExceptionBoundary.IgnoreException(task);
+        }
+
+        [Fact]
+        public async Task IgnoreExceptionAndDispose_Test()
+        {
+            var task = Task.FromException<int>(new OperationCanceledException());
+
+            // 例外を無視して終了する
+            await AsyncExceptionBoundary.IgnoreExceptionAndDispose(task);
+        }
+
+        [Fact]
+        public async Task IgnoreExceptionAndDispose_DisposeTest()
+        {
+            using var image = TestUtils.CreateDummyImage();
+            var task = Task.FromResult<object>(image); // IDisposable であることを静的に判定できない場合も想定
+
+            // 正常終了したとき Result が IDisposable だった場合は破棄する
+            await AsyncExceptionBoundary.IgnoreExceptionAndDispose(task);
+            Assert.True(image.IsDisposed);
+        }
+
+        [Fact]
+        public async Task IgnoreExceptionAndDispose_IEnumerableTest()
+        {
+            using var image1 = TestUtils.CreateDummyImage();
+            using var image2 = TestUtils.CreateDummyImage();
+            var tasks = new[]
+            {
+                Task.FromResult<object>(image1), // IDisposable であることを静的に判定できない場合も想定
+                Task.FromResult<object>(image2),
+            };
+
+            // 正常終了したとき Result が IDisposable だった場合は破棄する
+            await AsyncExceptionBoundary.IgnoreExceptionAndDispose(tasks);
+            Assert.True(image1.IsDisposed);
+            Assert.True(image2.IsDisposed);
+        }
+    }
+}
diff --git a/OpenTween/AsyncExceptionBoundary.cs b/OpenTween/AsyncExceptionBoundary.cs
new file mode 100644 (file)
index 0000000..61d7b6d
--- /dev/null
@@ -0,0 +1,73 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2024 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace OpenTween
+{
+    public static class AsyncExceptionBoundary
+    {
+        public static Task Wrap(Func<Task> func)
+            => Wrap(func, _ => true);
+
+        public static async Task Wrap(Func<Task> func, Func<Exception, bool> ignoreException)
+        {
+            try
+            {
+                await func();
+            }
+            catch (Exception ex) when (ignoreException(ex))
+            {
+            }
+        }
+
+        public static async Task IgnoreException(Task task)
+        {
+            try
+            {
+                await task.ConfigureAwait(false);
+            }
+            catch
+            {
+            }
+        }
+
+        public static async Task IgnoreExceptionAndDispose<T>(Task<T> task)
+        {
+            try
+            {
+                var ret = await task.ConfigureAwait(false);
+                (ret as IDisposable)?.Dispose();
+            }
+            catch
+            {
+            }
+        }
+
+        public static Task IgnoreExceptionAndDispose<T>(IEnumerable<Task<T>> tasks)
+            => Task.WhenAll(tasks.Select(x => IgnoreExceptionAndDispose(x)));
+    }
+}
index f167253..19173df 100644 (file)
@@ -56,25 +56,11 @@ namespace OpenTween
             this.InnerDictionary = new LRUCacheDictionary<string, Task<MemoryImage>>(trimLimit: 300, autoTrimCount: 100);
             this.InnerDictionary.CacheRemoved += (s, e) =>
             {
-                // まだ参照されている場合もあるのでDisposeはファイナライザ任せ
                 this.CacheRemoveCount++;
 
+                // まだ参照されている場合もあるのでDisposeはファイナライザ任せ
                 var task = e.Item.Value;
-                if (task.Status != TaskStatus.RanToCompletion || task.IsFaulted)
-                {
-                    // Task の例外がハンドルされないまま破棄されると AggregateException が発生するため try-catch で処理する Task を挟む
-                    static async Task HandleException<T>(Task<T> t)
-                    {
-                        try
-                        {
-                            _ = await t.ConfigureAwait(false);
-                        }
-                        catch
-                        {
-                        }
-                    }
-                    _ = HandleException(task);
-                }
+                _ = AsyncExceptionBoundary.IgnoreException(task);
             };
 
             this.cancelTokenSource = new CancellationTokenSource();
index 9569860..e70ad91 100644 (file)
@@ -59,21 +59,13 @@ namespace OpenTween
 
         private Task WrapAsyncFunc(Func<Task> func, bool runOnThreadPool)
         {
-            async Task TaskExceptionBoundary(Func<Task> func)
-            {
-                try
-                {
-                    await func();
-                }
-                catch (Exception ex) when (this.ignoreExceptionFunc(ex))
-                {
-                }
-            }
+            Task WrappedFunc()
+                => AsyncExceptionBoundary.Wrap(func, this.ignoreExceptionFunc);
 
             if (runOnThreadPool)
-                return Task.Run(() => TaskExceptionBoundary(func));
+                return Task.Run(WrappedFunc);
             else
-                return TaskExceptionBoundary(func);
+                return WrappedFunc();
         }
     }
 }
index 042a8d1..90c2621 100644 (file)
@@ -156,27 +156,7 @@ namespace OpenTween
         private void DisposeImages()
         {
             var oldImageTasks = this.loadImageTasks.OfType<Task<MemoryImage>>().ToArray();
-
-            static async Task DisposeTaskResults(Task<MemoryImage>[] tasks)
-            {
-                try
-                {
-                    await Task.WhenAll(tasks).ConfigureAwait(false);
-                }
-                catch
-                {
-                }
-
-                foreach (var task in tasks)
-                {
-                    if (task.IsFaulted || task.IsCanceled)
-                        continue;
-
-                    task.Result.Dispose();
-                }
-            }
-
-            _ = DisposeTaskResults(oldImageTasks);
+            _ = AsyncExceptionBoundary.IgnoreExceptionAndDispose(oldImageTasks);
         }
     }
 }