Skip to content

Resource Management - Dapr & Aspire #1576

Description

@abitofarchitecture

Describe the bug

CommunityToolkit.Aspire.Hosting.Dapr Version=13.0.0

I tried following 3 edge cases:

  1. When dependencies are managed outside of Aspire

`
using CommunityToolkit.Aspire.Hosting.Dapr;

var builder = DistributedApplication.CreateBuilder(args);

// The components directory is where the Dapr components will be placed for the Dapr sidecar to consume.
var componentsPath = Path.Combine(builder.AppHostDirectory, "components");

// The YAML file points to an external state store instance
var stateStore = builder.AddDaprStateStore("statestore", new DaprComponentOptions
{
// Path to your Dapr component YAML file - PostgreSQL state store
LocalPath = Path.Combine(componentsPath, "statestore.yaml")
});

// API Service
var apiService = builder.AddProject<Projects.AspireDapr_Demo_ApiService>("apiservice")
.WithHttpHealthCheck("/health")
.WithDaprSidecar(sidecar => sidecar
.WithOptions(new DaprSidecarOptions
{
AppId = "apiservice",
DaprHttpPort = 3500
})
.WithReference(stateStore));

builder.Build().Run();
`

Observations:

  • Incomplete resource visibility on Aspire dashboard e.g., PostgreSQL is not available on dashboard
  • Add cognitive overhead on developer to know the dependencies and it may not get all benefits of Aspire deployment
  1. When dependencies are managed by Aspire

`
using CommunityToolkit.Aspire.Hosting.Dapr;

var builder = DistributedApplication.CreateBuilder(args);

// The components directory is where the Dapr components will be placed for the Dapr sidecar to consume.
var componentsPath = Path.Combine(builder.AppHostDirectory, "components");

// PostgreSQL
var postgres = builder.AddPostgres("postgres")
.WithImageTag("17.6")
.WithContainerName("aspiredapr-demo-postgres")
.WithDataVolume("aspiredapr-demo-postgres-data")
.WithLifetime(ContainerLifetime.Persistent);

var appDb = postgres.AddDatabase("appdb");

// Get the endpoint information for the PostgreSQL component to use in the Dapr component configuration
var postgresEndpoint = postgres.GetEndpoint("tcp");

var stateStore = builder.AddDaprComponent(
"statestore",
"state.postgresql",
new DaprComponentOptions
{
LocalPath = Path.Combine(componentsPath, "statestore.yaml")
})
.WithMetadata("host", postgresEndpoint.Property(EndpointProperty.Host))
.WithMetadata("port", postgresEndpoint.Property(EndpointProperty.Port))
.WithMetadata("database", appDb.Resource.DatabaseName)
.WithMetadata("user", postgres.Resource.UserNameReference)
.WithMetadata("password", postgres.Resource.PasswordParameter!);

// API Service
var apiService = builder.AddProject<Projects.AspireDapr_Demo_ApiService>("apiservice")
.WithReference(appDb)
.WaitFor(postgres)
.WaitFor(appDb)
.WithHttpHealthCheck("/health")
.WithDaprSidecar(sidecar => sidecar
.WithOptions(new DaprSidecarOptions
{
AppId = "apiservice",
DaprHttpPort = 3500
})
.WithReference(stateStore)
.WaitFor(postgres)
.WaitFor(appDb));

builder.Build().Run();
`
Observations:

  • Race condition on first run e.g., application fails on first run as the Dapr Sidecar does not wait for dependent resources to reach a ready state before proceeding
  • Without a built-in readiness mechanism, the Dapr Sidecar start independently, make the first run unreliable but on subsequent run, it runs fine and the dashboard displays all resource properly
  1. When dependencies are managed by Aspire & Explicit Dapr Sidecar

`
var builder = DistributedApplication.CreateBuilder(args);

// The components directory is where the Dapr components will be placed for the Dapr sidecar to consume.
var componentsPath = Path.Combine(builder.AppHostDirectory, "components");
const string appDatabaseName = "appdb";
const string postgresContainerName = "aspiredapr-demo-postgres";

// PostgreSQL
var postgres = builder.AddPostgres("postgres")
.WithImageTag("17.6")
.WithEnvironment("POSTGRES_DB", appDatabaseName)
.WithContainerName(postgresContainerName)
.WithDataVolume("aspiredapr-demo-postgres-data")
.WithLifetime(ContainerLifetime.Persistent);

var appDb = postgres.AddDatabase(appDatabaseName, appDatabaseName);

// Get the endpoint information for the PostgreSQL component to use in the Dapr component configuration
var postgresEndpoint = postgres.GetEndpoint("tcp");

var postgresReady = builder.AddExecutable(
"postgres-ready",
"sh",
builder.AppHostDirectory,
"-c",
"""
until nc -z "$POSTGRES_HOST" "$POSTGRES_PORT"; do
sleep 1
done

    until docker exec aspiredapr-demo-postgres sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -h 127.0.0.1 -U postgres -d postgres -v ON_ERROR_STOP=1 -c "select 1"' >/dev/null 2>&1; do
      sleep 1
    done

    if ! docker exec aspiredapr-demo-postgres sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -h 127.0.0.1 -U postgres -d postgres -tAc "select 1 from pg_database where datname = '"'"'appdb'"'"'"' | grep -q 1; then
      docker exec aspiredapr-demo-postgres sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -h 127.0.0.1 -U postgres -d postgres -v ON_ERROR_STOP=1 -c "create database appdb"'
    fi

    until docker exec aspiredapr-demo-postgres sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -h 127.0.0.1 -U postgres -d appdb -v ON_ERROR_STOP=1 -c "select 1"' >/dev/null 2>&1; do
      sleep 1
    done
    """)
.WithEnvironment("POSTGRES_HOST", "127.0.0.1")
.WithEnvironment("POSTGRES_PORT", postgresEndpoint.Property(EndpointProperty.Port))
.WaitFor(postgres);

// API Service
var apiService = builder.AddProject<Projects.AspireDapr_Demo_ApiService>("apiservice")
.WithReference(appDb)
.WithEnvironment("DAPR_HTTP_PORT", "3500")
.WithEnvironment("DAPR_GRPC_PORT", "50001")
.WaitForCompletion(postgresReady)
.WithHttpHealthCheck("/health");

var apiEndpoint = apiService.GetEndpoint("http");

builder.AddExecutable(
"apiservice-dapr",
"dapr",
builder.AppHostDirectory,
"run",
"--app-id",
"apiservice",
"--resources-path",
componentsPath,
"--app-port",
apiEndpoint.Property(EndpointProperty.Port),
"--dapr-http-port",
"3500",
"--dapr-grpc-port",
"50001",
"--app-channel-address",
"localhost",
"--app-protocol",
"http")
.WithEnvironment("STATESTORE_HOST", "127.0.0.1")
.WithEnvironment("STATESTORE_PORT", postgresEndpoint.Property(EndpointProperty.Port))
.WithEnvironment("STATESTORE_USER", postgres.Resource.UserNameReference)
.WithEnvironment("postgres-password", postgres.Resource.PasswordParameter!)
.WaitForCompletion(postgresReady)
.WaitFor(apiService);

builder.Build().Run();
`

Observations:
This approach is not using CommunityToolkit.Aspire.Hosting.Dapr. Here Dapr runs as an explicit executable and additonal code needed to check the readiness check. It allows Aspire to properly sequence the startup order and dashboard look OK but not perfect.

  • Excessive custom code
  • Cluttered Aspire dashboard

Regression

No response

Steps to reproduce

.NET Version 10
Aspire version 13.4.0

Expected behavior

I would suggest to make fix for 2. When dependencies are managed by Aspire

The Dapr Sidecar should wait for dependent resources readiness.

Screenshots

No response

IDE and version

VS Code

IDE version

No response

Nuget packages

CommunityToolkit.Aspire.Hosting.Dapr Version=13.0.0

Additional context

No response

Help us help you

No, just wanted to report this

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions