Skip to content

Conversation

@ritchiecarroll
Copy link
Contributor

Merge development branch into master

@ritchiecarroll ritchiecarroll merged commit b33c93a into master Sep 4, 2025
1 of 2 checks passed
@ritchiecarroll ritchiecarroll deleted the development branch September 4, 2025 21:54
Comment on lines +134 to +137
catch (Exception ex)
{
LibraryEvents.OnSuppressedException(this, ex);
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.

Copilot Autofix

AI 5 months ago

The best practice in scenarios requiring exception suppression (e.g., inside Dispose patterns) is to only catch exceptions derived from Exception that are not considered fatal—that is, not system-critical exceptions such as OutOfMemoryException, StackOverflowException, ThreadAbortException, or the base SystemException. The catch-all block should be narrowed by adding a when filter that ignores fatal exceptions, or by explicitly catching only non-fatal exceptions.

In modern .NET, the recommended method is to exclude these fatal exceptions using a helper function, e.g., IsFatal(Exception), to determine if an exception is fatal before handling it/suppressing it. We can introduce this helper, and catch only if the exception is not fatal—otherwise, allow it to propagate.

To implement this, edit the catch block in the Dispose method in src/Gemstone.Threading/KeyedSignal.cs as follows:

  • Add a private static helper method, e.g., IsFatal(Exception ex), inside the same class.
  • Change the catch (Exception ex) line to include a when clause: catch (Exception ex) when (!IsFatal(ex)).

No additional dependencies are needed.


Suggested changeset 1
src/Gemstone.Threading/KeyedSignal.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Gemstone.Threading/KeyedSignal.cs b/src/Gemstone.Threading/KeyedSignal.cs
--- a/src/Gemstone.Threading/KeyedSignal.cs
+++ b/src/Gemstone.Threading/KeyedSignal.cs
@@ -131,7 +131,7 @@
             {
                 m_pruneTask.Wait();
             }
-            catch (Exception ex)
+            catch (Exception ex) when (!IsFatal(ex))
             {
                 LibraryEvents.OnSuppressedException(this, ex);
             }
@@ -403,4 +403,12 @@
     }
 
     #endregion
+    /// <summary>
+    /// Determines if the exception is considered fatal and should not be caught/suppressed.
+    /// </summary>
+    private static bool IsFatal(Exception ex) =>
+        ex is OutOfMemoryException
+            or StackOverflowException
+            or ThreadAbortException
+            or AccessViolationException;
 }
EOF
@@ -131,7 +131,7 @@
{
m_pruneTask.Wait();
}
catch (Exception ex)
catch (Exception ex) when (!IsFatal(ex))
{
LibraryEvents.OnSuppressedException(this, ex);
}
@@ -403,4 +403,12 @@
}

#endregion
/// <summary>
/// Determines if the exception is considered fatal and should not be caught/suppressed.
/// </summary>
private static bool IsFatal(Exception ex) =>
ex is OutOfMemoryException
or StackOverflowException
or ThreadAbortException
or AccessViolationException;
}
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +162 to +165
catch (Exception ex)
{
LibraryEvents.OnSuppressedException(this, ex);
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.

Copilot Autofix

AI 5 months ago

To fix the overly broad generic catch clause, narrow the set of caught exceptions in DisposeAsync. The most likely expected exception from await m_pruneTask.ConfigureAwait(false); is OperationCanceledException, given the cancellation logic. Other exceptions are either unexpected or indicative of deeper errors that might better be allowed to propagate or rethrow (or at least have more targeted handling).

Best approach:

  • Change catch (Exception ex) to catch (OperationCanceledException ex), which aligns with expected cancellation and suppression intent in Dispose.
  • Optionally, if other exception types are reasonably safe to suppress (e.g., ObjectDisposedException), catch and log those specifically.
  • If you must catch other exceptions (because disposal should never fail), add a second catch for generic Exception ex, log it, but document that this is intentional, or rethrow critical exceptions (such as those inheriting from SystemError).

Specifically:

  • In file src/Gemstone.Threading/KeyedSignal.cs:
    • Line 162: Replace catch (Exception ex) with catch (OperationCanceledException ex)
    • Optionally add a broader catch below for unexpected exceptions documenting why they're being suppressed (optional per project policy)
  • No new imports needed.

Suggested changeset 1
src/Gemstone.Threading/KeyedSignal.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Gemstone.Threading/KeyedSignal.cs b/src/Gemstone.Threading/KeyedSignal.cs
--- a/src/Gemstone.Threading/KeyedSignal.cs
+++ b/src/Gemstone.Threading/KeyedSignal.cs
@@ -159,10 +159,17 @@
             {
                 await m_pruneTask.ConfigureAwait(false);
             }
-            catch (Exception ex)
+            catch (OperationCanceledException ex)
             {
+                // Suppress expected cancellation exceptions during disposal.
                 LibraryEvents.OnSuppressedException(this, ex);
             }
+            // Optionally, handle and log other unexpected exceptions.
+            // Uncomment below if you wish to report/log all exceptions but not suppress critical errors:
+            //catch (Exception ex) when (!(ex is OutOfMemoryException || ex is StackOverflowException || ex is ThreadAbortException))
+            //{
+            //    LibraryEvents.OnSuppressedException(this, ex);
+            //}
 
             m_pruneTimer?.Dispose();
             m_pruneCompletionSource.Dispose();
EOF
@@ -159,10 +159,17 @@
{
await m_pruneTask.ConfigureAwait(false);
}
catch (Exception ex)
catch (OperationCanceledException ex)
{
// Suppress expected cancellation exceptions during disposal.
LibraryEvents.OnSuppressedException(this, ex);
}
// Optionally, handle and log other unexpected exceptions.
// Uncomment below if you wish to report/log all exceptions but not suppress critical errors:
//catch (Exception ex) when (!(ex is OutOfMemoryException || ex is StackOverflowException || ex is ThreadAbortException))
//{
// LibraryEvents.OnSuppressedException(this, ex);
//}

m_pruneTimer?.Dispose();
m_pruneCompletionSource.Dispose();
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +212 to +215
catch (Exception ex)
{
return (default!, ex);
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.

Copilot Autofix

AI 5 months ago

To fix the problem, replace the overly broad catch (Exception ex) block in the Wait method with more specific catch clauses for the exceptions that are part of normal operation and intended error modes. In this context, these likely include OperationCanceledException for cancellations and ObjectDisposedException for disposed object access. Any unhandled exceptions should be allowed to propagate to the caller. Make this change only in the Wait method, lines 212–215 in file src/Gemstone.Threading/KeyedSignal.cs, and do not change other code or imports unless essential.


Suggested changeset 1
src/Gemstone.Threading/KeyedSignal.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Gemstone.Threading/KeyedSignal.cs b/src/Gemstone.Threading/KeyedSignal.cs
--- a/src/Gemstone.Threading/KeyedSignal.cs
+++ b/src/Gemstone.Threading/KeyedSignal.cs
@@ -209,10 +209,14 @@
             TResult result = WaitAsync(key, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult();
             return (result, null);
         }
-        catch (Exception ex)
+        catch (OperationCanceledException ex)
         {
             return (default!, ex);
         }
+        catch (ObjectDisposedException ex)
+        {
+            return (default!, ex);
+        }
     }
 
     /// <summary>
EOF
@@ -209,10 +209,14 @@
TResult result = WaitAsync(key, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult();
return (result, null);
}
catch (Exception ex)
catch (OperationCanceledException ex)
{
return (default!, ex);
}
catch (ObjectDisposedException ex)
{
return (default!, ex);
}
}

/// <summary>
Copilot is powered by AI and may make mistakes. Always verify output.
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

Successfully merging this pull request may close these issues.

2 participants