Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On Demand Startup of Resources #5851

Closed
1 task done
afscrome opened this issue Sep 23, 2024 · 13 comments
Closed
1 task done

On Demand Startup of Resources #5851

afscrome opened this issue Sep 23, 2024 · 13 comments
Labels
area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication
Milestone

Comments

@afscrome
Copy link
Contributor

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

With the addition of being able to start/stop resources in the dashboard, it would be useful to be able to support resources which are started on demand rather than automatically on startup. A few use cases I can think of:

  1. Optional Local Dev dependencies - maybe your app has an expensive dependency that is only needed a small percentage of the time. It would be helpful to have this in your local dev dashboard, but only pay the cost of starting it up when you actually needed.
  2. CronJob resources deployed to Kubernetes - We have a few resources we deploy as nightly cron jobs to kuberentes. In local dev, it would be helpful to have these available
  3. Optional Second instance - most of the time, testing with a single . (Although perhaps this use case is better served by some kind of "Scale Replicas" command)

Describe the solution you'd like

Some kind of way to mark a resource so that Aspire doesn't automatically start the resource, instead waiting for the user to click the start button in the UI.

An alternative approach would be to provide a way for commands to dynamically add new resources, which would allow you to use a command to spawn up a new resource. This work well for options 2 & 3 above, but doesn't fit 1 so well.

Additional context

No response

@davidfowl davidfowl added area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication feature labels Sep 23, 2024
@davidlahuta
Copy link

davidlahuta commented Oct 13, 2024

CRUD interface to manage resources at runtime from application code is really needed.

We are working on software which deployes services (from docker images) as our users request them. The ablity to implement this with Aspire for local development would be awesome.

@davidfowl
Copy link
Member

Played with this a little based on what works today:

using Microsoft.Extensions.DependencyInjection;

var builder = DistributedApplication.CreateBuilder(args);

builder.AddContainer("redis", "redis").WithExplicitStart();

builder.Build().Run();

public static class ExplicitStartupExtensions
{
    public static IResourceBuilder<T> WithExplicitStart<T>(this IResourceBuilder<T> builder)
        where T : IResource
    {
        builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (evt, ct) =>
        {
            var rns = evt.Services.GetRequiredService<ResourceNotificationService>();

            // This is possibly the last safe place to update the resource's annotations
            // we need to do it this late because the built in lifecycle annotations are added *very* late
            var startCommand = evt.Resource.Annotations.OfType<ResourceCommandAnnotation>().FirstOrDefault(c => c.Type == "resource-start");

            if (startCommand is null)
            {
                return;
            }

            evt.Resource.Annotations.Remove(startCommand);

            // This will block the resource from starting until the "resource-start" command is executed
            var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

            // Create a new command that clones the start command
            var newCommand = new ResourceCommandAnnotation(
                startCommand.Type,
                startCommand.DisplayName,
                context =>
                {
                    if (!tcs.Task.IsCompleted)
                    {
                        return ResourceCommandState.Enabled;
                    }

                    return startCommand.UpdateState(context);
                },
                context =>
                {
                    if (!tcs.Task.IsCompleted)
                    {
                        tcs.SetResult();
                        return Task.FromResult(CommandResults.Success());
                    }

                    return startCommand.ExecuteCommand(context);
                },
                startCommand.DisplayDescription,
                startCommand.Parameter,
                startCommand.ConfirmationMessage,
                startCommand.IconName,
                startCommand.IconVariant,
                startCommand.IsHighlighted);

            evt.Resource.Annotations.Add(newCommand);

            await rns.PublishUpdateAsync(evt.Resource, s => s with { State = new(KnownResourceStates.Waiting, KnownResourceStateStyles.Info) });

            await tcs.Task.WaitAsync(ct);
        });

        return builder;
    }
}

@davidlahuta
Copy link

davidlahuta commented Oct 16, 2024

Thank you for the insight, but it's not quite what I have in mind.

I can envision an interface to manage Aspire resources, with its instance accessible from a running project in Aspire.
Let’s write some pseudocode.

IAspireResourceManager  
{
  IAspireResource[] GetResources();
  IAspireResource GetResource(name);
  IAspireResource AddResource(name, ...)
}

IAspireResouce 
{
  Task<string> GetState();
  Task WaitFor();
  Task Remove();
  ....
  string GetConnectionString()
}

// from running Aspire application project, let's say Blazor App
IAspireResourceManager resourceManager = .... get instance from DI

resourceManager.AddResource("worker-user-1", ...);
var worker = resourceManager.GetResource("worker-user-1");
await worker.WaitFor();
...
}

@davidfowl
Copy link
Member

Sorry, I wasn't replying to your request for an api to dynamically add resources, I think that's a different, harder and longer term feature request. The other features in this issue are quite doable with existing APIs.

@WouterBau
Copy link

I had the same cronjob situation as @afscrome (Thank you for sending the issue) and used the suggested solution.
I only had to tweak some small bits to have it work with the latest version of Aspire.
This would be an interesting new feature for Aspire.

public static class ExplicitStartupExtensions
{
    public static IResourceBuilder<T> WithExplicitStart<T>(this IResourceBuilder<T> builder)
        where T : IResource
    {
        builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (evt, ct) =>
        {
            var rns = evt.Services.GetRequiredService<ResourceNotificationService>();

            // This is possibly the last safe place to update the resource's annotations
            // we need to do it this late because the built in lifecycle annotations are added *very* late
            var startCommand = evt.Resource.Annotations.OfType<ResourceCommandAnnotation>().FirstOrDefault(c => c.Name == "resource-start");

            if (startCommand is null)
            {
                return;
            }

            evt.Resource.Annotations.Remove(startCommand);

            // This will block the resource from starting until the "resource-start" command is executed
            var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

            // Create a new command that clones the start command
            var newCommand = new ResourceCommandAnnotation(
                startCommand.Name,
                startCommand.DisplayName,
                context => !tcs.Task.IsCompleted ? ResourceCommandState.Enabled : startCommand.UpdateState(context),
                context =>
                {
                    if (tcs.Task.IsCompleted)
                        return startCommand.ExecuteCommand(context);

                    tcs.SetResult();
                    return Task.FromResult(CommandResults.Success());
                },
                startCommand.DisplayDescription,
                startCommand.Parameter,
                startCommand.ConfirmationMessage,
                startCommand.IconName,
                startCommand.IconVariant,
                startCommand.IsHighlighted);

            evt.Resource.Annotations.Add(newCommand);

            await rns.PublishUpdateAsync(evt.Resource, s => s with { State = new ResourceStateSnapshot(KnownResourceStates.Waiting, KnownResourceStateStyles.Info) });

            await tcs.Task.WaitAsync(ct);
        });

        return builder;
    }
}

@davidfowl
Copy link
Member

@JamesNK when you are investigating #5879, can you see how cheap it would be to implement this? I think that refactoring is a prereq for this.

@davidfowl davidfowl marked this as a duplicate of #7053 Jan 14, 2025
@davidfowl davidfowl marked this as a duplicate of #7043 Jan 15, 2025
@JamesNK
Copy link
Member

JamesNK commented Jan 26, 2025

I think a way to mark a resource as not automatically starting is a good place to begin here.

@davidfowl Questions:

  1. What do you think the API would look like? builder.AddProject<MyConsoleApp>("app").WithExplicitStart()?
  2. This would be just for containers, executables, projects? They're the only resources that are automatically started today.
  3. What would the state of the resources be? Maybe we introduce a new state like NotStarted?
  4. I'm guessing a WaitFor on a resource that doesn't automatically start would work like you'd expect? It would just wait.
  5. One way to implement this would to not run CreateContainerAsync or CreateExecutableAsync for a resource during DCP startup. Then, when start resource command is executed, check to see whether the resource has been created in DCP or not. If it hasn't then create it, if it has then do the current restart logic.
  6. Another implementation option would be to run the create container/executable methods on startup so the resource is created in DCP, but pass DCP a flag to tell it not to start the resource.
  7. Anything else I'm not thinking of?

@JamesNK
Copy link
Member

JamesNK commented Jan 26, 2025

@karolz-ms Do you have thoughts here about resources that don't startup automatically?

@davidfowl
Copy link
Member

What do you think the API would look like? builder.AddProject("app").WithExplicitStart()?

Either that or something like that means don't start.

This would be just for containers, executables, projects? They're the only resources that are automatically started today.

And azure resources, so it'll be an annotation that a resource could choose to implement.

What would the state of the resources be? Maybe we introduce a new state like NotStarted?

That's probably clearest, but Stopped might be ok.

One way to implement this would to not run CreateContainerAsync or CreateExecutableAsync for a resource during DCP startup. Then, when start resource command is executed, check to see whether the resource has been created in DCP or not. If it hasn't then create it, if it has then do the current restart logic.

Yep. That works.

Another implementation option would be to run the create container/executable methods on startup so the resource is created in DCP, but pass DCP a flag to tell it not to start the resource.

Unclear if you want to resolve environment variables and raise events before the user starts. I think the resource should be completely cold and nothing will run until you start it.

@afscrome
Copy link
Contributor Author

I'd love it if there was a way to programmatically start such resources. For example, the following could be used to run database migrations when the database becomes ready.

var sql = builder.AddSqlServer("sql")
    .WithLifetime(ContainerLifetime.Persistent)
    ;

var db = sql.AddDatabase("mydb");

var migrator = builder.AddExecutable("db-migrations", "dotnet-ef", "..")
    .WithArgs("database",
    "update",
    "--connection", db.Resource.ConnectionStringExpression,
    "--project", new AspireInitialisation_DbMigrations().ProjectPath,
    "--no-build",
    "-v")
    .WithExplicitStart() // Or whatever API is deccided
    ;

builder.Eventing.Subscribe<ResourceReadyEvent>(async (evt, ct) =>
{
    var rns = evt.Services.GetRequiredService<ResourceNotificationService>();

    migrator.Start(); // or some API for equalivent functionality.
    await rns.WaitForResourceAsync(evt.Resource.Name, KnownResourceStates.Finished, ct);
});

@davidfowl
Copy link
Member

davidfowl commented Jan 27, 2025

@afscrome I appreciate that you’re find creative ways to accomplish your scenarios given what works today. I’d encourage you to file issues / discussions with the full end to end experiences that you’re building for your team so we can figure out the best approach and building blocks required

@ArnoldIsHereToday
Copy link

I am wondering if this is the exact same situation for implementing it as described in this issue, but the scenario is very similar:

I'd love to manually start support-components like PgAdmin or Redis Commander.

var postgresql = builder.AddPostgres("Postgresql")
    .WithPgAdmin(c => c.WithExplicitStart());
var cache = builder.AddRedis("Cache")
    .WithRedisCommander(c => c.WithExplicitStart());
var mailDev = builder.AddContainer("Maildev", "maildev/maildev").WithExplicitStart();

They are great when I need them and I love the Aspire experience, but I only need them for some tasks.
Manually starting them will not impact anything else since no other components will require them.

@davidfowl
Copy link
Member

This is done in 9.1 #7324

@davidfowl davidfowl modified the milestones: Backlog, 9.1 Jan 31, 2025
@github-actions github-actions bot locked and limited conversation to collaborators Mar 2, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication
Projects
None yet
Development

No branches or pull requests

7 participants