diff --git a/src/Backends/DotCompute.Backends.CUDA/Persistent/CudaPersistentKernelManager.cs b/src/Backends/DotCompute.Backends.CUDA/Persistent/CudaPersistentKernelManager.cs index 7b12a0e8f..29541d154 100644 --- a/src/Backends/DotCompute.Backends.CUDA/Persistent/CudaPersistentKernelManager.cs +++ b/src/Backends/DotCompute.Backends.CUDA/Persistent/CudaPersistentKernelManager.cs @@ -14,7 +14,7 @@ namespace DotCompute.Backends.CUDA.Persistent /// /// Manages persistent, grid-resident CUDA kernels for long-running computations. /// - public sealed partial class CudaPersistentKernelManager : IDisposable + public sealed partial class CudaPersistentKernelManager : IDisposable, IAsyncDisposable { #region LoggerMessage Delegates @@ -337,6 +337,43 @@ public void Dispose() _ringBufferAllocator?.Dispose(); _disposed = true; } + + /// + /// Asynchronously disposes the persistent kernel manager, awaiting the + /// shutdown of every active persistent kernel before releasing the + /// ring-buffer allocator. + /// + /// + /// Prefer this over in async teardown: each + /// StopKernelAsync is awaited rather than resolved via + /// GetAwaiter().GetResult(), so a kernel that takes non-trivial + /// time to drain its command queue does not block a thread-pool thread. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + // Stop all active kernels cooperatively. + foreach (var kernelId in _activeKernels.Keys) + { + try + { + await StopKernelAsync(kernelId).ConfigureAwait(false); + } + catch (Exception ex) + { + LogKernelStopError(_logger, ex); + } + } + + _ringBufferAllocator?.Dispose(); + _disposed = true; + + GC.SuppressFinalize(this); + } /// /// A class that represents persistent kernel state. /// @@ -401,7 +438,7 @@ public void Dispose() /// /// Handle for interacting with a running persistent kernel. /// - public interface IPersistentKernelHandle : IDisposable + public interface IPersistentKernelHandle : IDisposable, IAsyncDisposable { /// /// Gets or sets the kernel identifier. @@ -512,6 +549,24 @@ public void Dispose() // Swallow exceptions during dispose } } + + /// + /// Asynchronously stops the kernel and releases resources without + /// blocking the caller's thread. + /// + public async ValueTask DisposeAsync() + { + try + { + await StopAsync().ConfigureAwait(false); + } + catch (Exception) + { + // Swallow exceptions during dispose + } + + GC.SuppressFinalize(this); + } } /// diff --git a/src/Backends/DotCompute.Backends.CUDA/Profiling/CudaPerformanceProfiler.cs b/src/Backends/DotCompute.Backends.CUDA/Profiling/CudaPerformanceProfiler.cs index c574cc53a..d0413983a 100644 --- a/src/Backends/DotCompute.Backends.CUDA/Profiling/CudaPerformanceProfiler.cs +++ b/src/Backends/DotCompute.Backends.CUDA/Profiling/CudaPerformanceProfiler.cs @@ -16,7 +16,7 @@ namespace DotCompute.Backends.CUDA.Profiling /// Production-grade CUDA performance profiler with CUPTI integration, /// metrics collection, and detailed performance analysis. /// - public sealed partial class CudaPerformanceProfiler : IDisposable + public sealed partial class CudaPerformanceProfiler : IDisposable, IAsyncDisposable { private readonly ILogger _logger; private readonly ConcurrentDictionary _kernelProfiles; @@ -786,5 +786,56 @@ public void Dispose() _disposed = true; } + + /// + /// Asynchronously disposes the profiler, awaiting any active profiling + /// session's stop-and-drain instead of blocking the calling thread. + /// + /// + /// Prefer this over in async shutdown paths: when + /// profiling is active, StopProfilingAsync is awaited rather than + /// resolved via GetAwaiter().GetResult(), so a long-running flush + /// of CUPTI buffers does not stall the caller's thread pool worker. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _metricsTimer?.Dispose(); + _profilingLock?.Dispose(); + + if (_isProfilingActive) + { + try + { + _ = await StopProfilingAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + Trace.TraceWarning($"StopProfilingAsync failed during async disposal: {ex.Message}"); + } + } + + if (_cuptiSubscriber != IntPtr.Zero) + { + _ = cuptiUnsubscribe(_cuptiSubscriber); + } + + try + { + _ = nvmlShutdown(); + } + catch (Exception ex) + { + Trace.TraceWarning($"NVML shutdown failed: {ex.Message}"); + } + + _disposed = true; + + GC.SuppressFinalize(this); + } } } diff --git a/src/Backends/DotCompute.Backends.Metal/MetalBackend.LoggerMessages.cs b/src/Backends/DotCompute.Backends.Metal/MetalBackend.LoggerMessages.cs index 0a175f956..69cd1fb46 100644 --- a/src/Backends/DotCompute.Backends.Metal/MetalBackend.LoggerMessages.cs +++ b/src/Backends/DotCompute.Backends.Metal/MetalBackend.LoggerMessages.cs @@ -166,4 +166,10 @@ public partial class MetalBackend Level = LogLevel.Error, Message = "Failed to create Metal accelerator for device {DeviceIndex}")] private static partial void LogAcceleratorCreationError(ILogger logger, Exception ex, int deviceIndex); + + [LoggerMessage( + EventId = 3002, + Level = LogLevel.Warning, + Message = "Failed to dispose Metal accelerator during backend shutdown")] + private static partial void LogAcceleratorDisposeError(ILogger logger, Exception ex); } diff --git a/src/Backends/DotCompute.Backends.Metal/MetalBackend.cs b/src/Backends/DotCompute.Backends.Metal/MetalBackend.cs index 1ed4b8793..573ac43a9 100644 --- a/src/Backends/DotCompute.Backends.Metal/MetalBackend.cs +++ b/src/Backends/DotCompute.Backends.Metal/MetalBackend.cs @@ -17,7 +17,7 @@ namespace DotCompute.Backends.Metal; /// /// Main entry point for Metal compute backend /// -public sealed partial class MetalBackend : IDisposable +public sealed partial class MetalBackend : IDisposable, IAsyncDisposable { private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; @@ -499,4 +499,44 @@ public void Dispose() _disposed = true; GC.SuppressFinalize(this); } + + /// + /// Asynchronously disposes the backend by awaiting each Metal accelerator's + /// in sequence. + /// + /// + /// Prefer this over in async hosts: Metal accelerators + /// hold command queues, buffer pools, and MPS handles whose clean shutdown + /// involves asynchronous work. Awaiting here avoids the + /// AsTask().GetAwaiter().GetResult() sync-over-async pattern and + /// prevents thread-pool starvation during multi-GPU teardown. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + foreach (var accelerator in _accelerators) + { + if (accelerator is null) + { + continue; + } + + try + { + await accelerator.DisposeAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + LogAcceleratorDisposeError(_logger, ex); + } + } + + _accelerators.Clear(); + _disposed = true; + GC.SuppressFinalize(this); + } } diff --git a/src/Core/DotCompute.Core/Logging/LogBuffer.cs b/src/Core/DotCompute.Core/Logging/LogBuffer.cs index ccbabe83d..f9098499c 100644 --- a/src/Core/DotCompute.Core/Logging/LogBuffer.cs +++ b/src/Core/DotCompute.Core/Logging/LogBuffer.cs @@ -13,7 +13,7 @@ namespace DotCompute.Core.Logging; /// High-performance asynchronous log buffer with batching, compression, and multiple sink support. /// Designed to minimize performance impact on the main application thread while ensuring reliable log delivery. /// -public sealed partial class LogBuffer : IDisposable +public sealed partial class LogBuffer : IDisposable, IAsyncDisposable { #region LoggerMessage Delegates @@ -621,6 +621,87 @@ public void Dispose() _cancellationTokenSource?.Dispose(); } } + + /// + /// Asynchronously disposes the log buffer, completing the channel writer, + /// awaiting the processing task, and performing a final flush without + /// blocking the calling thread. + /// + /// + /// Prefer this over when shutting down inside an + /// async scope (host teardown, test cleanup): the background processing + /// task is awaited rather than Wait(TimeSpan)'d, and the final + /// flush happens asynchronously, so shutdown does not stall an async + /// thread pool worker. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + + try + { + // Stop accepting new entries. + _writer.Complete(); + + // Cancel background processing. + await _cancellationTokenSource.CancelAsync().ConfigureAwait(false); + + // Await the background processing task (bounded by a timeout so + // a wedged processor cannot hang shutdown). + try + { + await _processingTask.WaitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + } + catch (TimeoutException) + { + _logger.LogWarningMessage("Log processing task did not complete within timeout"); + } + catch (OperationCanceledException) + { + // Expected when the processing task observes cancellation. + } + + // Final flush, awaited rather than sync-over-async. + try + { + await FlushAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogErrorMessage(ex, "Error during final LogBuffer flush"); + } + + // Dispose sinks. ILogSink is IDisposable today; this matches Dispose(). + foreach (var sink in _sinks) + { + try + { + sink.Dispose(); + } + catch (Exception ex) + { + _logger.LogErrorMessage(ex, $"Error disposing sink: {sink.GetType().Name}"); + } + } + } + catch (Exception ex) + { + _logger.LogErrorMessage(ex, "Error during LogBuffer async disposal"); + } + finally + { + _batchTimer?.Dispose(); + _flushSemaphore?.Dispose(); + _cancellationTokenSource?.Dispose(); + } + + GC.SuppressFinalize(this); + } } /// /// An i log sink interface. diff --git a/src/Core/DotCompute.Core/Logging/StructuredLogger.cs b/src/Core/DotCompute.Core/Logging/StructuredLogger.cs index 6129bc3ed..5606aa758 100644 --- a/src/Core/DotCompute.Core/Logging/StructuredLogger.cs +++ b/src/Core/DotCompute.Core/Logging/StructuredLogger.cs @@ -11,7 +11,7 @@ namespace DotCompute.Core.Logging; /// Production-grade structured logger with semantic properties, correlation IDs, and performance metrics. /// Provides async, buffered logging with configurable sinks and minimal performance impact. /// -public sealed partial class StructuredLogger : ILogger, IDisposable +public sealed partial class StructuredLogger : ILogger, IDisposable, IAsyncDisposable { private readonly string _categoryName; private readonly ILogger _baseLogger; @@ -609,6 +609,40 @@ public void Dispose() _flushTimer?.Dispose(); } + + /// + /// Asynchronously disposes the logger, performing a final flush of the underlying + /// without blocking the calling thread. + /// + /// + /// Prefer this over in async hosts: the buffered channel is + /// drained via rather than a sync-over-async + /// FlushAsync().GetAwaiter().GetResult(), so the processing task can complete + /// cleanly without starving the thread pool during shutdown. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + + try + { + // Flush and shut down the buffer cooperatively. + await _logBuffer.DisposeAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + LogFinalFlushFailed(_baseLogger, ex); + } + + _flushTimer?.Dispose(); + + GC.SuppressFinalize(this); + } } /// /// A class that represents structured logging options. diff --git a/src/Core/DotCompute.Core/Security/MemoryProtection.cs b/src/Core/DotCompute.Core/Security/MemoryProtection.cs index ca4501f40..b51795472 100644 --- a/src/Core/DotCompute.Core/Security/MemoryProtection.cs +++ b/src/Core/DotCompute.Core/Security/MemoryProtection.cs @@ -18,7 +18,7 @@ namespace DotCompute.Core.Security; /// Provides comprehensive memory protection services including bounds checking, /// guard pages, and secure memory management with defense against common memory vulnerabilities. /// -public sealed partial class MemoryProtection : IDisposable +public sealed partial class MemoryProtection : IDisposable, IAsyncDisposable { private readonly ILogger _logger; private readonly MemoryProtectionConfiguration _configuration; @@ -896,6 +896,52 @@ public void Dispose() _logger.LogInfoMessage($"MemoryProtection disposed. Final statistics: Allocations={_allocations.Count}, Violations={_violationCount}"); } + + /// + /// Asynchronously disposes the memory protection service, awaiting secure + /// wipes of any protected regions before releasing their backing memory. + /// + /// + /// Prefer this over when disposing during async + /// shutdown: each region's secure wipe is awaited rather than resolved via + /// GetAwaiter().GetResult(), which avoids blocking the thread pool + /// when many regions need to be scrubbed. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + + _integrityCheckTimer?.Dispose(); + _allocationLock?.Dispose(); + + foreach (var region in _protectedRegions.Values) + { + try + { + if (_configuration.EnableSecureWiping) + { + await SecureWipeMemoryAsync(region).ConfigureAwait(false); + } + FreeRawMemory(region.BaseAddress, region.TotalSize); + } + catch (Exception ex) + { + MemoryRegionDisposeError(_logger, ex, region.Identifier); + } + } + + _protectedRegions.Clear(); + _allocations.Clear(); + + _logger.LogInfoMessage($"MemoryProtection disposed. Final statistics: Allocations={_allocations.Count}, Violations={_violationCount}"); + + GC.SuppressFinalize(this); + } } #region Supporting Types diff --git a/src/Core/DotCompute.Core/Security/MemorySanitizer.cs b/src/Core/DotCompute.Core/Security/MemorySanitizer.cs index d6d629822..b5db88486 100644 --- a/src/Core/DotCompute.Core/Security/MemorySanitizer.cs +++ b/src/Core/DotCompute.Core/Security/MemorySanitizer.cs @@ -22,7 +22,7 @@ namespace DotCompute.Core.Security; /// with advanced security features for protecting sensitive data in memory. /// Implements production-grade memory safety with hardware-accelerated validation. /// -public sealed partial class MemorySanitizer : IDisposable +public sealed partial class MemorySanitizer : IDisposable, IAsyncDisposable { private readonly ILogger _logger; private readonly MemorySanitizerConfiguration _configuration; @@ -1085,5 +1085,55 @@ public void Dispose() var stats = GetStatistics(); LogSanitizerDisposed(_logger, stats.TotalAllocations, stats.TotalViolations); } + + /// + /// Asynchronously disposes the sanitizer, awaiting secure wipes of any + /// tracked allocations before releasing their backing memory. + /// + /// + /// Prefer this over in async shutdown paths: each + /// tracked allocation's secure wipe is awaited rather than resolved via + /// GetAwaiter().GetResult(), which prevents starving the thread pool + /// when many allocations need to be scrubbed at once. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + + // Secure-wipe + free each tracked allocation asynchronously. + foreach (var allocation in _trackedAllocations.Values) + { + try + { + if (_configuration.EnableSecureWiping) + { + await PerformSecureWipeAsync(allocation).ConfigureAwait(false); + } + FreeRawMemory(allocation.BaseAddress, allocation.TotalSize); + } + catch (Exception ex) + { + LogAllocationDisposeError(_logger, ex, allocation.Identifier); + } + } + + _trackedAllocations.Clear(); + _freeHistory.Clear(); + + _leakDetectionTimer?.Dispose(); + _integrityCheckTimer?.Dispose(); + _operationLock?.Dispose(); + _randomGenerator?.Dispose(); + + var stats = GetStatistics(); + LogSanitizerDisposed(_logger, stats.TotalAllocations, stats.TotalViolations); + + GC.SuppressFinalize(this); + } } diff --git a/src/Extensions/DotCompute.Algorithms/Security/KernelSandbox.cs b/src/Extensions/DotCompute.Algorithms/Security/KernelSandbox.cs index eaad1ff00..2c1f18269 100644 --- a/src/Extensions/DotCompute.Algorithms/Security/KernelSandbox.cs +++ b/src/Extensions/DotCompute.Algorithms/Security/KernelSandbox.cs @@ -15,7 +15,7 @@ namespace DotCompute.Algorithms.Security; /// Provides sandboxed execution environment for untrusted kernels with comprehensive security controls. /// Implements process isolation, resource limits, and execution monitoring. /// -public sealed partial class KernelSandbox : IDisposable +public sealed partial class KernelSandbox : IDisposable, IAsyncDisposable { private readonly ILogger _logger; private readonly SandboxConfiguration _configuration; @@ -546,6 +546,54 @@ public void Dispose() _logger.LogInfoMessage("KernelSandbox disposed"); } + /// + /// Asynchronously disposes the sandbox, awaiting each active sandbox's + /// cooperative teardown before releasing the monitoring timer and + /// creation lock. + /// + /// + /// Prefer this over in async shutdown paths: + /// destroying each sandbox spawns a process-kill-and-cleanup task; awaiting + /// them via instead of + /// avoids parking a + /// thread-pool worker for up to 30 seconds while many sandboxes unwind. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + + _monitoringTimer?.Dispose(); + _creationLock?.Dispose(); + + var sandboxTasks = _activeSandboxes.Keys + .Select(DestroySandboxInstanceAsync) + .ToArray(); + + try + { + await Task.WhenAll(sandboxTasks) + .WaitAsync(TimeSpan.FromSeconds(30)) + .ConfigureAwait(false); + } + catch (TimeoutException ex) + { + _logger.LogErrorMessage(ex, "Timeout async-disposing sandboxes"); + } + catch (Exception ex) + { + _logger.LogErrorMessage(ex, "Error async-disposing sandboxes"); + } + + _logger.LogInfoMessage("KernelSandbox async-disposed"); + + GC.SuppressFinalize(this); + } + #region LoggerMessage Delegates [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to set directory permissions for: {TempPath}")] diff --git a/src/Extensions/DotCompute.Linq/CodeGeneration/CompilationPipeline.cs b/src/Extensions/DotCompute.Linq/CodeGeneration/CompilationPipeline.cs index 3785da72d..d6c88f1b7 100644 --- a/src/Extensions/DotCompute.Linq/CodeGeneration/CompilationPipeline.cs +++ b/src/Extensions/DotCompute.Linq/CodeGeneration/CompilationPipeline.cs @@ -44,7 +44,7 @@ namespace DotCompute.Linq.CodeGeneration; /// The internal Roslyn workspace is reused across compilations for efficiency. /// /// -public sealed class CompilationPipeline : IDisposable +public sealed class CompilationPipeline : IDisposable, IAsyncDisposable { private readonly IKernelCache _kernelCache; private readonly CpuKernelGenerator _kernelGenerator; @@ -987,6 +987,47 @@ public void Dispose() Interlocked.Read(ref _compilationFailures), Interlocked.Read(ref _fallbackCount)); } + + /// + /// Asynchronously disposes the compilation pipeline, awaiting the CUDA + /// and Metal runtime compilers' cooperative shutdown so their semaphores + /// and cached kernels release without blocking the calling thread. + /// + /// + /// Prefer this over in async shutdown paths: the + /// CUDA and Metal compilers each own a guarding + /// compilation state and a dictionary of cached compiled kernels that may + /// implement . Awaiting their + /// drains both cooperatively. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + + if (_cudaCompiler is not null) + { + await _cudaCompiler.DisposeAsync().ConfigureAwait(false); + } + + if (_metalCompiler is not null) + { + await _metalCompiler.DisposeAsync().ConfigureAwait(false); + } + + _logger?.LogInformation( + "CompilationPipeline async-disposed. Stats - Total: {Total}, Cache Hits: {Hits}, Failures: {Failures}, Fallbacks: {Fallbacks}", + Interlocked.Read(ref _totalCompilations), + Interlocked.Read(ref _cacheHits), + Interlocked.Read(ref _compilationFailures), + Interlocked.Read(ref _fallbackCount)); + + GC.SuppressFinalize(this); + } } /// diff --git a/src/Extensions/DotCompute.Linq/CodeGeneration/RuntimeExecutor.cs b/src/Extensions/DotCompute.Linq/CodeGeneration/RuntimeExecutor.cs index 20b6951aa..2612a4a58 100644 --- a/src/Extensions/DotCompute.Linq/CodeGeneration/RuntimeExecutor.cs +++ b/src/Extensions/DotCompute.Linq/CodeGeneration/RuntimeExecutor.cs @@ -37,7 +37,7 @@ namespace DotCompute.Linq.CodeGeneration; /// Execute compiled GPU kernels on the appropriate accelerator /// /// -public sealed class RuntimeExecutor : IDisposable +public sealed class RuntimeExecutor : IDisposable, IAsyncDisposable { private readonly ILogger _logger; private readonly Dictionary _accelerators = new(); @@ -543,4 +543,39 @@ public void Dispose() _logger.LogInformation("RuntimeExecutor disposed"); } + + /// + /// Asynchronously disposes all accelerators and releases resources. + /// + /// + /// Prefer this over in async shutdown paths: each + /// accelerator's is awaited + /// directly instead of resolved via AsTask().Wait(), so long-running + /// resource cleanup (stream synchronization, memory pool trimming) does + /// not block the calling thread. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) return; + + foreach (var accelerator in _accelerators.Values) + { + try + { + await accelerator.DisposeAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error async-disposing accelerator: {Name}", accelerator.Info.Name); + } + } + + _accelerators.Clear(); + _initLock.Dispose(); + _disposed = true; + + _logger.LogInformation("RuntimeExecutor async-disposed"); + + GC.SuppressFinalize(this); + } } diff --git a/src/Extensions/DotCompute.Linq/Compilation/CudaRuntimeKernelCompiler.cs b/src/Extensions/DotCompute.Linq/Compilation/CudaRuntimeKernelCompiler.cs index 500660d0a..74ecb0dfb 100644 --- a/src/Extensions/DotCompute.Linq/Compilation/CudaRuntimeKernelCompiler.cs +++ b/src/Extensions/DotCompute.Linq/Compilation/CudaRuntimeKernelCompiler.cs @@ -45,7 +45,7 @@ namespace DotCompute.Linq.Compilation; /// Production-ready error handling and fallback to CPU /// /// -public sealed class CudaRuntimeKernelCompiler : IGpuKernelCompiler, IDisposable +public sealed class CudaRuntimeKernelCompiler : IGpuKernelCompiler, IDisposable, IAsyncDisposable { private readonly ILogger _logger; private readonly CudaAccelerator _accelerator; @@ -311,6 +311,54 @@ public void Dispose() _disposed = true; } } + + /// + /// Asynchronously disposes the CUDA runtime kernel compiler, acquiring + /// the compilation semaphore cooperatively so in-flight compilation calls + /// can drain before cached kernels are released. + /// + /// + /// Prefer this over in async shutdown paths: the + /// compilation semaphore is acquired via + /// instead of a blocking + /// , and any cached kernels that + /// implement are awaited rather than + /// sync-disposed. This avoids starving the thread pool when a concurrent + /// CompileKernelAsync holds the semaphore. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) return; + + await _compilationLock.WaitAsync().ConfigureAwait(false); + try + { + foreach (var kernel in _compiledKernels.Values) + { + if (kernel is IAsyncDisposable asyncKernel) + { + await asyncKernel.DisposeAsync().ConfigureAwait(false); + } + else + { + (kernel as IDisposable)?.Dispose(); + } + } + + _compiledKernels.Clear(); + + _logger.LogInformation("CudaRuntimeKernelCompiler async-disposed ({Count} cached kernels cleaned up)", + _compiledKernels.Count); + } + finally + { + _ = _compilationLock.Release(); + _compilationLock.Dispose(); + _disposed = true; + } + + GC.SuppressFinalize(this); + } } /// diff --git a/src/Extensions/DotCompute.Linq/Compilation/MetalRuntimeKernelCompiler.cs b/src/Extensions/DotCompute.Linq/Compilation/MetalRuntimeKernelCompiler.cs index a04420f64..f8eae1bb5 100644 --- a/src/Extensions/DotCompute.Linq/Compilation/MetalRuntimeKernelCompiler.cs +++ b/src/Extensions/DotCompute.Linq/Compilation/MetalRuntimeKernelCompiler.cs @@ -44,7 +44,7 @@ namespace DotCompute.Linq.Compilation; /// iOS/iPadOS devices (A-series chips) /// /// -public sealed class MetalRuntimeKernelCompiler : IGpuKernelCompiler, IDisposable +public sealed class MetalRuntimeKernelCompiler : IGpuKernelCompiler, IDisposable, IAsyncDisposable { private readonly ILogger _logger; private readonly MetalAccelerator _accelerator; @@ -305,6 +305,53 @@ public void Dispose() _disposed = true; } } + + /// + /// Asynchronously disposes the Metal runtime kernel compiler, acquiring + /// the compilation semaphore cooperatively so in-flight compilation calls + /// can drain before cached kernels are released. + /// + /// + /// Prefer this over in async shutdown paths: the + /// compilation semaphore is acquired via + /// instead of a blocking + /// , and any cached kernels that + /// implement are awaited rather than + /// sync-disposed. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) return; + + await _compilationLock.WaitAsync().ConfigureAwait(false); + try + { + foreach (var kernel in _compiledKernels.Values) + { + if (kernel is IAsyncDisposable asyncKernel) + { + await asyncKernel.DisposeAsync().ConfigureAwait(false); + } + else + { + (kernel as IDisposable)?.Dispose(); + } + } + + _compiledKernels.Clear(); + + _logger.LogInformation("MetalRuntimeKernelCompiler async-disposed ({Count} cached kernels cleaned up)", + _compiledKernels.Count); + } + finally + { + _ = _compilationLock.Release(); + _compilationLock.Dispose(); + _disposed = true; + } + + GC.SuppressFinalize(this); + } } /// diff --git a/src/Extensions/DotCompute.Linq/Reactive/BackpressureManager.cs b/src/Extensions/DotCompute.Linq/Reactive/BackpressureManager.cs index 5269e324a..c0df42b1c 100644 --- a/src/Extensions/DotCompute.Linq/Reactive/BackpressureManager.cs +++ b/src/Extensions/DotCompute.Linq/Reactive/BackpressureManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace DotCompute.Linq.Reactive; @@ -20,7 +21,7 @@ namespace DotCompute.Linq.Reactive; /// - Block: Block producer thread until space available /// - Sample: Keep only most recent item /// -public sealed class BackpressureManager : IBackpressureManager, IDisposable +public sealed class BackpressureManager : IBackpressureManager, IDisposable, IAsyncDisposable { private readonly ILogger _logger; @@ -361,19 +362,81 @@ public void ResetStatistics() #endregion - #region IDisposable + #region IDisposable / IAsyncDisposable + + private int _disposed; /// /// Disposes resources used by the backpressure manager. /// public void Dispose() { - _cancellationSource.Cancel(); + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + DisposeCore(); + } + + /// + /// Asynchronously disposes resources used by the backpressure manager. + /// + /// + /// Cancels the cancellation source, unblocks any producer threads waiting on + /// , and drains the pending buffer before + /// releasing resources. Prefer this over when running inside + /// async contexts so producers observe cancellation cleanly before shutdown. + /// + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + try + { + await _cancellationSource.CancelAsync().ConfigureAwait(false); + } + catch (ObjectDisposedException) + { + // Already disposed - nothing to cancel. + } + + // Wake any producers blocked in HandleBlock so they can observe cancellation + // before we tear the manager down. + UnblockProducer(); + + // Drain pending items so subscribers that still hold a reference see an + // empty buffer rather than a half-populated snapshot after disposal. + while (_buffer.TryDequeue(out _)) + { + } + _cancellationSource.Dispose(); + _logger.LogInformation("BackpressureManager disposed. " + + "Total processed: {Processed}, Total dropped: {Dropped}", + _totalItemsProcessed, _totalItemsDropped); + } + + private void DisposeCore() + { + try + { + _cancellationSource.Cancel(); + } + catch (ObjectDisposedException) + { + // Already disposed - nothing to cancel. + } + // Unblock any waiting threads UnblockProducer(); + _cancellationSource.Dispose(); + _logger.LogInformation("BackpressureManager disposed. " + "Total processed: {Processed}, Total dropped: {Dropped}", _totalItemsProcessed, _totalItemsDropped); diff --git a/src/Runtime/DotCompute.Plugins/Managers/NuGetPluginManager.cs b/src/Runtime/DotCompute.Plugins/Managers/NuGetPluginManager.cs index 38609d9ff..5996e672b 100644 --- a/src/Runtime/DotCompute.Plugins/Managers/NuGetPluginManager.cs +++ b/src/Runtime/DotCompute.Plugins/Managers/NuGetPluginManager.cs @@ -17,7 +17,7 @@ namespace DotCompute.Plugins.Managers; /// /// Advanced NuGet plugin manager with comprehensive lifecycle management, hot reloading, and monitoring. /// -public class NuGetPluginManager : IDisposable +public class NuGetPluginManager : IDisposable, IAsyncDisposable { private readonly ILogger _logger; private readonly NuGetPluginLoader _pluginLoader; @@ -676,6 +676,49 @@ public void Dispose() GC.SuppressFinalize(this); } + + /// + /// Asynchronously disposes the plugin manager, awaiting each plugin's + /// unload before tearing down the loader, health monitor, and metrics + /// collector. + /// + /// + /// Prefer this over during async host shutdown: + /// UnloadPluginAsync is awaited rather than resolved via + /// GetAwaiter().GetResult(), so long-running unload hooks (plugin + /// finalisation, terminator scripts) do not block a thread-pool worker. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + + _periodicMaintenanceTimer?.Dispose(); + + // Unload all plugins cooperatively. + foreach (var (pluginId, _) in _managedPlugins.ToList()) + { + try + { + _ = await UnloadPluginAsync(pluginId).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogErrorMessage(ex, $"Error unloading plugin during async dispose: {pluginId}"); + } + } + + _pluginLoader?.Dispose(); + _healthMonitor?.Dispose(); + _metricsCollector?.Dispose(); + _operationSemaphore?.Dispose(); + + GC.SuppressFinalize(this); + } } /// diff --git a/src/Runtime/DotCompute.Plugins/Security/PluginSandbox.cs b/src/Runtime/DotCompute.Plugins/Security/PluginSandbox.cs index 0b23928d5..7431ebc72 100644 --- a/src/Runtime/DotCompute.Plugins/Security/PluginSandbox.cs +++ b/src/Runtime/DotCompute.Plugins/Security/PluginSandbox.cs @@ -12,7 +12,7 @@ namespace DotCompute.Plugins.Security; /// /// Provides secure sandboxing and isolation for plugin execution with controlled permissions. /// -public class PluginSandbox : IDisposable +public class PluginSandbox : IDisposable, IAsyncDisposable { private readonly ILogger _logger; private readonly SandboxConfiguration _configuration; @@ -467,4 +467,42 @@ public void Dispose() _resourceMonitor?.Dispose(); _securityManager?.Dispose(); } + + /// + /// Asynchronously disposes the sandbox, awaiting each sandboxed plugin's + /// termination before releasing the security manager and resource monitor. + /// + /// + /// Prefer this over in async contexts: plugin + /// termination runs cooperative cleanup hooks that previously resolved via + /// GetAwaiter().GetResult(), which can deadlock under hosts with a + /// captured synchronization context and starve the thread pool when many + /// plugins are active. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + GC.SuppressFinalize(this); + + foreach (var plugin in _sandboxedPlugins.Values) + { + try + { + await plugin.TerminateAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogErrorMessage(ex, $"Error async-disposing sandboxed plugin {plugin.Id}"); + } + } + + _sandboxedPlugins.Clear(); + _resourceMonitor?.Dispose(); + _securityManager?.Dispose(); + } } diff --git a/src/Runtime/DotCompute.Plugins/Security/SandboxedPlugin.cs b/src/Runtime/DotCompute.Plugins/Security/SandboxedPlugin.cs index 2e50a6d19..ab482b3fa 100644 --- a/src/Runtime/DotCompute.Plugins/Security/SandboxedPlugin.cs +++ b/src/Runtime/DotCompute.Plugins/Security/SandboxedPlugin.cs @@ -6,7 +6,7 @@ namespace DotCompute.Plugins.Security; /// /// Represents a plugin running in a secure sandbox with restricted permissions. /// -public class SandboxedPlugin : IDisposable +public class SandboxedPlugin : IDisposable, IAsyncDisposable { private readonly ResourceMonitor _resourceMonitor; private bool _disposed; @@ -175,6 +175,37 @@ public void Dispose() TerminateAsync().GetAwaiter().GetResult(); GC.SuppressFinalize(this); } + + /// + /// Asynchronously disposes the sandboxed plugin, awaiting its termination + /// hooks before releasing the isolated load context and resource monitor. + /// + /// + /// Prefer this over in async contexts: plugin + /// termination runs sensitive cleanup (flushing outstanding operations, + /// clearing cached data) that previously resolved via + /// GetAwaiter().GetResult(), which can deadlock on hosts with a + /// captured synchronization context. + /// + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + try + { + await TerminateAsync().ConfigureAwait(false); + } + catch + { + // Swallow exceptions during async disposal - matches the sync path. + } + + GC.SuppressFinalize(this); + } } /// diff --git a/tests/Unit/DotCompute.Core.Tests/Security/MemoryProtectionTests.cs b/tests/Unit/DotCompute.Core.Tests/Security/MemoryProtectionTests.cs index dd79891e9..1dafa759b 100644 --- a/tests/Unit/DotCompute.Core.Tests/Security/MemoryProtectionTests.cs +++ b/tests/Unit/DotCompute.Core.Tests/Security/MemoryProtectionTests.cs @@ -713,7 +713,7 @@ public async Task AllocateProtectedMemoryAsync_AfterDispose_ShouldThrowObjectDis { // Arrange var protection = new MemoryProtection(_logger); - protection.Dispose(); + await protection.DisposeAsync(); // Act var action = async () => await protection.AllocateProtectedMemoryAsync(1024); diff --git a/tests/Unit/DotCompute.Core.Tests/Security/MemorySanitizerTests.cs b/tests/Unit/DotCompute.Core.Tests/Security/MemorySanitizerTests.cs index 59029e9d8..b7291b361 100644 --- a/tests/Unit/DotCompute.Core.Tests/Security/MemorySanitizerTests.cs +++ b/tests/Unit/DotCompute.Core.Tests/Security/MemorySanitizerTests.cs @@ -718,7 +718,7 @@ public async Task AllocateSanitizedMemoryAsync_AfterDispose_ShouldThrowObjectDis { // Arrange var sanitizer = new MemorySanitizer(_logger); - sanitizer.Dispose(); + await sanitizer.DisposeAsync(); // Act var action = async () => await sanitizer.AllocateSanitizedMemoryAsync(1024); @@ -762,7 +762,7 @@ public async Task DetectMemoryLeaksAsync_AfterDispose_ShouldThrowObjectDisposedE { // Arrange var sanitizer = new MemorySanitizer(_logger); - sanitizer.Dispose(); + await sanitizer.DisposeAsync(); // Act var action = sanitizer.DetectMemoryLeaksAsync; diff --git a/tests/Unit/DotCompute.Plugins.Tests/Security/PluginSandboxTests.cs b/tests/Unit/DotCompute.Plugins.Tests/Security/PluginSandboxTests.cs index 334fc6d3c..f1b4d13d3 100644 --- a/tests/Unit/DotCompute.Plugins.Tests/Security/PluginSandboxTests.cs +++ b/tests/Unit/DotCompute.Plugins.Tests/Security/PluginSandboxTests.cs @@ -119,7 +119,7 @@ public async Task CreateSandboxedPluginAsync_WhenDisposed_ShouldThrowObjectDispo { // Arrange _sandbox = new PluginSandbox(_mockLogger); - _sandbox.Dispose(); + await _sandbox.DisposeAsync(); var permissions = new SandboxPermissions(); // Act @@ -174,7 +174,7 @@ public async Task TerminatePluginAsync_WhenDisposed_ShouldThrowObjectDisposedExc { // Arrange _sandbox = new PluginSandbox(_mockLogger); - _sandbox.Dispose(); + await _sandbox.DisposeAsync(); // Act var act = async () => await _sandbox.TerminatePluginAsync(Guid.NewGuid());