Skip to content

Commit

Permalink
Extract all data acquisition from ResourceService (#1288)
Browse files Browse the repository at this point in the history
* Extract all data acquisition from ResourceService

This creates a new class `KubernetesDataSource` that encapsulates all code related to obtaining data from kubernetes in the `ResourceService`. This cleans the service code up a lot and makes the responsibilities clearer.

The use of `Channel<ResourceChange>` has been removed. Instead, code invocations are made directly, skipping queuing. We still have two other channels that ensure front/back are decoupled. The kubernetes channel will ultimately also be removed, leaving just a channel for the subscriber.

Added some API docs.

* Check cancellation token

* Remove Channel<T> for kubernetes objects

This is the second removal of a `Channel<T>` from this component. Now, updates are passed by direct code invocations, skipping queuing. We still have a downstream channel for the subscriber to decouple front/back.

* Inline method

* Rename ResourceCollection to ResourcePublisher

* Simplify inclusion of docker environment

A few changes here:

- Use more consistent names throughout. They're not "extra" or "additional" arguments. In fact they are a complete replacement. In their absence, the "spec" environment is used.
- Move creation of docker inspection task to view model method, making the various "handle * update" methods more uniform, for future refactoring.
- Remove special handling in `ProcessKubernetesChange` for one scenario. Simplifies the signature, and the code a fair bit.

* Specify list capacity

* Merge ObjectChangeType Add and Modified

We only need to know if the value is "upserted" (updated or inserted), or "deleted".

Recent changes to threading/queuing in the code here changed some timing. The arrival of one kind of resouce can trigger the publication of another, and these would always be "modified", however they could arrive before that resource's stream published that instance. The rest of the code handles these happening out-of-order. We just need to treat add and modified the same way, so they've been merged.

* Use semaphore to serialize kubernetes data processing

There are multiple kubernetes resource types, each with its own monitoring stream. Across these resources, updates arrive concurrently. The update flows for each type of resource can interact with state stored for other resources. Therefore we use a semaphore to ensure that only one resource update can flow through the system at a time.

Another option would have been to make the collections concurrent, or use explicit locking. Concurrent collections are heavy. Explicit locking is tricky to get right. This mutual exclusion via top-level semaphore seems like a safe and elegant approach for now.

* Avoid redundant linear scans

When this method is passed the same collection twice, the `FromSpec` value will always be true, because every item in the list is in the list. Instead we pass `null` and consider all items as from the spec.

* Simplify delegation of projects-as-executables

* Avoid switch on every update

Each instance of `WatchKubernetesResource<T>` produces a single type, that would flow through `ProcessKubernetesChange` to look up the relevant handler. Instead, pass the handler in to `WatchKubernetesResource<T>` so it can be invoked directly.

* Formatting

* Rename class to avoid name conflict

This object is a snapshot of a service's state, not a service itself. We have another class which is a service. Append "Snapshot" to differentiate. Future work will extend this concept of snapshots more broadly.

* Replace O(N) scanning with O(1) lookup

* Make resource Endpoints and Services immutable

* Make ResourceViewModel.Environment immutable

Note that it's just the collection that becomes immutable here. The elements are still currently mutable. That will change when we split front/back ends, and have a snapshot on the backend with a view model on the front end.

* Make ContainerViewModel Ports and Args immutable

* Make ExecutableViewModel Arguments immutable

* Make remaining scalar ExecutableViewModel properties immutable

* Make all resource snapshot properties required

* Add API docs for resource snapshot types

* Rename KubernetesDataSource to DcpDataSource

* Use collection literals for empty collections

* Source container variables from status

This data is now available from DCP, so we don't have to launch processes to query docker for this data any more, which simplifies things quite nicely.

* Allocate less memory in ProcessUtil

* Extract duplicate code

* Further deduplicate code

* Make ProjectViewModel derive from ExecutableViewModel

* Remove duplicate razor after consolidating types

* Merge project/executable snapshot construction

Now that project snapshots derive from executable snapshots, we can unify a bunch of construction logic.

* Replace O(N) scanning with O(1) lookup

* Reorder methods

* Start using "snapshot" naming in DCP data source

We still have some types named ViewModel, but they'll be renamed later in the split.

* Rename ObjectChangeType to ResourceChangeType

It's not clear what object the name refers to, so make it more specific. It's internal, so we can always rename it again later. This name pairs nicely with `ObjectChange` too.

* Further consolidate data processing

The methods that handle executable and container updates are largely the same. Extract that commonality to a new method.
  • Loading branch information
drewnoakes committed Dec 11, 2023
1 parent 818ac20 commit 1f56e60
Show file tree
Hide file tree
Showing 20 changed files with 558 additions and 719 deletions.
12 changes: 5 additions & 7 deletions src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,15 +187,12 @@ private async Task HandleSelectedOptionChangedAsync()
NavigationManager.NavigateTo($"/ConsoleLogs/{_selectedOption?.Value}");
}

private async Task OnResourceListChangedAsync(ObjectChangeType changeType, ResourceViewModel resourceViewModel)
private async Task OnResourceListChangedAsync(ResourceChangeType changeType, ResourceViewModel resourceViewModel)
{
if (changeType == ObjectChangeType.Added)
{
_resourceNameMapping[resourceViewModel.Name] = resourceViewModel;
}
else if (changeType == ObjectChangeType.Modified)
if (changeType == ResourceChangeType.Upsert)
{
_resourceNameMapping[resourceViewModel.Name] = resourceViewModel;

if (string.Equals(_selectedResource?.Name, resourceViewModel.Name, StringComparison.Ordinal))
{
_selectedResource = resourceViewModel;
Expand All @@ -210,9 +207,10 @@ private async Task OnResourceListChangedAsync(ObjectChangeType changeType, Resou
}
}
}
else if (changeType == ObjectChangeType.Deleted)
else if (changeType == ResourceChangeType.Deleted)
{
_resourceNameMapping.Remove(resourceViewModel.Name);

if (string.Equals(_selectedResource?.Name, resourceViewModel.Name, StringComparison.Ordinal))
{
_selectedOption = _noSelection;
Expand Down
14 changes: 5 additions & 9 deletions src/Aspire.Dashboard/Components/Pages/Resources.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ private int GetUnviewedErrorCount(ResourceViewModel resource)

private void ShowEnvironmentVariables(ResourceViewModel resource)
{
if (SelectedEnvironmentVariables == resource.Environment)
if (SelectedResource == resource)
{
ClearSelectedResource();
}
Expand All @@ -147,19 +147,15 @@ private void ClearSelectedResource()
SelectedResource = null;
}

private async Task OnResourceListChanged(ObjectChangeType objectChangeType, ResourceViewModel resource)
private async Task OnResourceListChanged(ResourceChangeType changeType, ResourceViewModel resource)
{
switch (objectChangeType)
switch (changeType)
{
case ObjectChangeType.Added:
_resourcesMap.Add(resource.Name, resource);
break;

case ObjectChangeType.Modified:
case ResourceChangeType.Upsert:
_resourcesMap[resource.Name] = resource;
break;

case ObjectChangeType.Deleted:
case ResourceChangeType.Deleted:
_resourcesMap.Remove(resource.Name);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<FluentStack Orientation="Orientation.Vertical">
@* If we have no endpoints, and the app isn't running anymore or we're not expecting any, then just say None *@
@if (Resource.Endpoints.Count == 0 && (Resource.State == FinishedState || Resource.ExpectedEndpointsCount == 0))
@if (Resource.Endpoints.Length == 0 && (Resource.State == FinishedState || Resource.ExpectedEndpointsCount == 0))
{
<span class="long-inner-content">None</span>
}
Expand All @@ -15,7 +15,7 @@
}
@* If we're expecting more, say Starting..., unless the app isn't running anymore *@
if (Resource.State != FinishedState
&& (Resource.ExpectedEndpointsCount is null || Resource.ExpectedEndpointsCount > Resource.Endpoints.Count))
&& (Resource.ExpectedEndpointsCount is null || Resource.ExpectedEndpointsCount > Resource.Endpoints.Length))
{
<span class="long-inner-content">Starting...</span>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@

<FluentStack Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
<span><FluentHighlighter HighlightedText="@FilterText" Text="@FormatName(Resource)" /></span>
@if (Resource is ProjectViewModel projectViewModel)
{
var title = $"Process ID: {projectViewModel.ProcessId}";
<span class="subtext" title="@title" aria-label="@title">@projectViewModel.ProcessId</span>
}
else if (Resource is ContainerViewModel containerViewModel)
@if (Resource is ContainerViewModel containerViewModel)
{
<div class="subtext">
<GridValue Value="@containerViewModel.ContainerId"
Expand All @@ -19,6 +14,7 @@
}
else if (Resource is ExecutableViewModel executableViewModel)
{
// NOTE projects are also executables, so this will handle both
var title = $"Process ID: {executableViewModel.ProcessId}";
<span class="subtext" title="@title" aria-label="@title">@executableViewModel.ProcessId</span>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

@if (Resource is ProjectViewModel projectViewModel)
{
// NOTE projects are also executables, so we have to check for projects first
<span title="@projectViewModel.ProjectPath" aria-label="@projectViewModel.ProjectPath">@Path.GetFileName(projectViewModel.ProjectPath)</span>
}
else if (Resource is ExecutableViewModel executableViewModel)
Expand All @@ -18,9 +19,9 @@ else if (Resource is ContainerViewModel containerViewModel)
var ports = string.Join("; ", containerViewModel.Ports);
<FluentStack Orientation="Orientation.Horizontal">
<span title="@containerViewModel.Image"><FluentHighlighter HighlightedText="@FilterText" Text="@containerViewModel.Image" /></span>
@if (containerViewModel.Ports.Count > 0)
@if (containerViewModel.Ports.Length > 0)
{
var title = $"Port{(containerViewModel.Ports.Count > 1 ? "s" : string.Empty)}: {ports}";
var title = $"Port{(containerViewModel.Ports.Length > 1 ? "s" : string.Empty)}: {ports}";
<span class="subtext" title="@title" aria-label="@title">@ports</span>
}
</FluentStack>
Expand Down
13 changes: 9 additions & 4 deletions src/Aspire.Dashboard/Model/ContainerViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Aspire.Dashboard.Extensions;

namespace Aspire.Dashboard.Model;

/// <summary>
/// Immutable snapshot of container state at a point in time.
/// </summary>
public class ContainerViewModel : ResourceViewModel
{
public override string ResourceType => "Container";
public string? ContainerId { get; init; }

public required string? ContainerId { get; init; }
public required string Image { get; init; }
public List<int> Ports { get; } = new();
public string? Command { get; init; }
public List<string>? Args { get; init; }
public required ImmutableArray<int> Ports { get; init; }
public required string? Command { get; init; }
public required ImmutableArray<string>? Args { get; init; }

internal override bool MatchesFilter(string filter)
{
Expand Down
14 changes: 10 additions & 4 deletions src/Aspire.Dashboard/Model/ExecutableViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;

namespace Aspire.Dashboard.Model;

/// <summary>
/// Immutable snapshot of executable state at a point in time.
/// </summary>
public class ExecutableViewModel : ResourceViewModel
{
public override string ResourceType => "Executable";
public int? ProcessId { get; init; }
public string? ExecutablePath { get; set; }
public string? WorkingDirectory { get; set; }
public List<string>? Arguments { get; set; }

public required int? ProcessId { get; init; }
public required string? ExecutablePath { get; init; }
public required string? WorkingDirectory { get; init; }
public required ImmutableArray<string>? Arguments { get; init; }
}

12 changes: 0 additions & 12 deletions src/Aspire.Dashboard/Model/ObjectChangeType.cs

This file was deleted.

7 changes: 5 additions & 2 deletions src/Aspire.Dashboard/Model/ProjectViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

namespace Aspire.Dashboard.Model;

public class ProjectViewModel : ResourceViewModel
/// <summary>
/// Immutable snapshot of project state at a point in time.
/// </summary>
public class ProjectViewModel : ExecutableViewModel
{
public override string ResourceType => "Project";
public int? ProcessId { get; init; }

public required string ProjectPath { get; init; }
}
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Model/ResourceChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

namespace Aspire.Dashboard.Model;

public sealed record ResourceChange(ObjectChangeType ObjectChangeType, ResourceViewModel Resource);
public sealed record ResourceChange(ResourceChangeType ChangeType, ResourceViewModel Resource);
19 changes: 19 additions & 0 deletions src/Aspire.Dashboard/Model/ResourceChangeType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Dashboard.Model;

public enum ResourceChangeType
{
Other,

/// <summary>
/// The object was added if new, or updated if not.
/// </summary>
Upsert,

/// <summary>
/// The object was deleted.
/// </summary>
Deleted
}
12 changes: 4 additions & 8 deletions src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,13 @@ public ResourceOutgoingPeerResolver(IResourceService resourceService)
});
}

private async Task OnResourceListChanged(ObjectChangeType changeType, ResourceViewModel resourceViewModel)
private async Task OnResourceListChanged(ResourceChangeType changeType, ResourceViewModel resourceViewModel)
{
if (changeType == ObjectChangeType.Added)
if (changeType == ResourceChangeType.Upsert)
{
_resourceNameMapping[resourceViewModel.Name] = resourceViewModel;
}
else if (changeType == ObjectChangeType.Modified)
{
_resourceNameMapping[resourceViewModel.Name] = resourceViewModel;
}
else if (changeType == ObjectChangeType.Deleted)
else if (changeType == ResourceChangeType.Deleted)
{
_resourceNameMapping.TryRemove(resourceViewModel.Name, out _);
}
Expand Down Expand Up @@ -93,7 +89,7 @@ private void RemoveSubscription(ModelSubscription subscription)

private async Task RaisePeerChangesAsync()
{
if (_subscriptions.Count == 0)
if (_subscriptions.Count == 0 || _watchContainersTokenSource.IsCancellationRequested)
{
return;
}
Expand Down
19 changes: 12 additions & 7 deletions src/Aspire.Dashboard/Model/ResourceViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using Aspire.Dashboard.Extensions;

namespace Aspire.Dashboard.Model;

/// <summary>
/// Base class for immutable snapshots of resource state at a point in time.
/// </summary>
public abstract class ResourceViewModel
{
public required string Name { get; init; }
public required string DisplayName { get; init; }
public required string Uid { get; init; }
public string? State { get; init; }
public DateTime? CreationTimeStamp { get; init; }
public List<EnvironmentVariableViewModel> Environment { get; } = new();
public required string? State { get; init; }
public required DateTime? CreationTimeStamp { get; init; }
public required ImmutableArray<EnvironmentVariableViewModel> Environment { get; init; }
public required ILogSource LogSource { get; init; }
public List<string> Endpoints { get; } = new();
public List<ResourceService> Services { get; } = new();
public int? ExpectedEndpointsCount { get; init; }
public required ImmutableArray<string> Endpoints { get; init; }
public required ImmutableArray<ResourceServiceSnapshot> Services { get; init; }
public required int? ExpectedEndpointsCount { get; init; }

public abstract string ResourceType { get; }

public static string GetResourceName(ResourceViewModel resource, IEnumerable<ResourceViewModel> allResources)
Expand All @@ -43,7 +48,7 @@ internal virtual bool MatchesFilter(string filter)
}
}

public sealed class ResourceService(string name, string? allocatedAddress, int? allocatedPort)
public sealed class ResourceServiceSnapshot(string name, string? allocatedAddress, int? allocatedPort)
{
public string Name { get; } = name;
public string? AllocatedAddress { get; } = allocatedAddress;
Expand Down
Loading

0 comments on commit 1f56e60

Please sign in to comment.