| File: ServiceClient\DashboardDataSourcePool.cs | Web Access |
| Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Dashboard.Otlp.Storage; namespace Aspire.Dashboard.ServiceClient; /// <summary> /// Manages dashboard data sources and shares historical databases across dashboard circuits. /// </summary> public sealed class DashboardDataSourcePool : IDisposable { private static readonly StringComparer s_pathComparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; private readonly object _lock = new(); private readonly Dictionary<string, Entry> _entries = new(s_pathComparer); private readonly IDashboardRunStore _runStore; private readonly IRepositoryFactory _repositoryFactory; private Lease? _current; private bool _disposed; /// <summary> /// Initializes a new instance of the <see cref="DashboardDataSourcePool"/> class. /// </summary> /// <param name="runStore">The dashboard run store used to locate and lease dashboard runs.</param> /// <param name="repositoryFactory">The factory used to create repositories for dashboard runs.</param> public DashboardDataSourcePool(IDashboardRunStore runStore, IRepositoryFactory repositoryFactory) { _runStore = runStore; _repositoryFactory = repositoryFactory; } /// <summary> /// Gets the pool-owned data source lease for the current dashboard run. /// </summary> /// <remarks> /// Callers must not dispose the returned lease. It is shared by all callers and disposed with the pool. /// </remarks> public Lease Current { get { lock (_lock) { ObjectDisposedException.ThrowIf(_disposed, this); var currentRun = _runStore.GetCurrentRun(); if (_current is null) { var database = new DashboardSqliteDatabase(currentRun.DatabasePath); _current = new Lease( database, () => _repositoryFactory.CreateTelemetryRepository(database), () => _repositoryFactory.CreateResourceRepository(database), () => { database.ClearPool(); database.Dispose(); }); } return _current; } } } /// <summary> /// Attempts to acquire a shared data source lease for the specified dashboard run. /// </summary> /// <param name="run">The dashboard run to acquire.</param> /// <returns>A shared data source lease, or <see langword="null"/> when the run is no longer available.</returns> public Lease? TryAcquire(DashboardRunDescriptor run) { lock (_lock) { ObjectDisposedException.ThrowIf(_disposed, this); var databasePath = Path.GetFullPath(run.DatabasePath); if (!_entries.TryGetValue(databasePath, out var entry)) { var runLease = _runStore.TryAcquireRunLease(run); if (runLease is null) { return null; } DashboardSqliteDatabase? database = null; try { database = new DashboardSqliteDatabase(databasePath, readOnly: true); entry = new Entry(database, runLease); _entries.Add(databasePath, entry); } catch { database?.ClearPool(); database?.Dispose(); runLease.Dispose(); throw; } } entry.ReferenceCount++; try { if (!entry.Database.ValidateSchemaVersion(run.SchemaVersion)) { throw new InvalidOperationException( $"Dashboard database for run '{run.RunId}' does not match run metadata schema version '{run.SchemaVersion}'."); } } catch { Release(entry); throw; } var lease = new Lease( entry.Database, () => _repositoryFactory.CreateTelemetryRepository(entry.Database), () => _repositoryFactory.CreateResourceRepository(entry.Database), () => Release(entry)); try { _ = lease.TelemetryRepository; _ = lease.ResourceRepository; return lease; } catch { lease.Dispose(); throw; } } } internal async Task InitializeAsync(CancellationToken cancellationToken) { await Current.Database.InitializeSchemaAsync(cancellationToken).ConfigureAwait(false); } internal void PublishRun() { _runStore.PublishRun(); } internal void PruneExpiredRuns() { _runStore.PruneExpiredRuns(); } private void Release(Entry entry) { lock (_lock) { if (entry.IsDisposed) { return; } entry.ReferenceCount--; if (entry.ReferenceCount == 0) { _entries.Remove(entry.Database.DatabasePath); DisposeEntry(entry); } } } /// <inheritdoc/> public void Dispose() { lock (_lock) { if (_disposed) { return; } _disposed = true; foreach (var entry in _entries.Values) { DisposeEntry(entry); } _entries.Clear(); _current?.Dispose(); } } private static void DisposeEntry(Entry entry) { entry.IsDisposed = true; try { entry.Database.ClearPool(); entry.Database.Dispose(); } finally { entry.RunLease.Dispose(); } } private sealed class Entry(DashboardSqliteDatabase database, IDisposable runLease) { public DashboardSqliteDatabase Database { get; } = database; public IDisposable RunLease { get; } = runLease; public int ReferenceCount { get; set; } public bool IsDisposed { get; set; } } /// <summary> /// Keeps repositories and their shared dashboard database available until the lease is disposed. /// </summary> public sealed class Lease : IDisposable { private readonly Lazy<ITelemetryRepository> _telemetryRepository; private readonly Lazy<IResourceRepository> _resourceRepository; private readonly Action _release; private int _disposed; internal Lease( DashboardSqliteDatabase database, Func<ITelemetryRepository> telemetryRepositoryFactory, Func<IResourceRepository> resourceRepositoryFactory, Action release) { Database = database; _telemetryRepository = new(telemetryRepositoryFactory); _resourceRepository = new(resourceRepositoryFactory); _release = release; } /// <summary> /// Gets the shared dashboard database. /// </summary> public DashboardSqliteDatabase Database { get; } /// <summary> /// Gets the telemetry repository for the leased dashboard run. /// </summary> public ITelemetryRepository TelemetryRepository => _telemetryRepository.Value; /// <summary> /// Gets the resource repository for the leased dashboard run. /// </summary> public IResourceRepository ResourceRepository => _resourceRepository.Value; /// <inheritdoc/> public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) == 0) { try { if (_telemetryRepository.IsValueCreated) { _telemetryRepository.Value.Dispose(); } } finally { try { if (_resourceRepository.IsValueCreated) { (_resourceRepository.Value as IDisposable)?.Dispose(); } } finally { _release(); } } } } } }