Skip to content

feat: [#783] Add app.Run(...runner) function to optimize the app running process#1221

Merged
hwbrzzl merged 12 commits intomasterfrom
bowen/#783
Oct 16, 2025
Merged

feat: [#783] Add app.Run(...runner) function to optimize the app running process#1221
hwbrzzl merged 12 commits intomasterfrom
bowen/#783

Conversation

@hwbrzzl
Copy link
Contributor

@hwbrzzl hwbrzzl commented Oct 11, 2025

📑 Description

Closes https://github.com/goravel/goravel/issues/

Related goravel/goravel#785

  1. Add a new Run function to run Route, Grpc, Queue, and Schedule.
  2. Add a new Ctx function to listen to the quit signal.

✅ Checks

  • Added test cases for my code

@codecov
Copy link

codecov bot commented Oct 12, 2025

Codecov Report

❌ Patch coverage is 48.70130% with 79 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.95%. Comparing base (e78715c) to head (4bd410c).
⚠️ Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
foundation/application.go 78.12% 20 Missing and 1 partial ⚠️
queue/runners.go 0.00% 15 Missing ⚠️
schedule/runners.go 0.00% 12 Missing ⚠️
grpc/runners.go 0.00% 11 Missing ⚠️
route/runners.go 0.00% 11 Missing ⚠️
grpc/service_provider.go 0.00% 3 Missing ⚠️
queue/service_provider.go 0.00% 2 Missing ⚠️
route/service_provider.go 0.00% 2 Missing ⚠️
schedule/service_provider.go 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1221      +/-   ##
==========================================
- Coverage   66.66%   65.95%   -0.71%     
==========================================
  Files         237      243       +6     
  Lines       15879    16676     +797     
==========================================
+ Hits        10585    10998     +413     
- Misses       4928     5298     +370     
- Partials      366      380      +14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 968517a Previous: e78715c Ratio
BenchmarkFile_ReadWrite 357473 ns/op 6257 B/op 99 allocs/op 198025 ns/op 6257 B/op 99 allocs/op 1.81
BenchmarkFile_ReadWrite - ns/op 357473 ns/op 198025 ns/op 1.81

This comment was automatically generated by workflow using github-action-benchmark.

@hwbrzzl hwbrzzl changed the title feat: [#783] Add bootstrap.Run(...facade) function to optimize the app running process feat: [#783] Add app.Run(...module) function to optimize the app running process Oct 12, 2025
return App
}

func (r *Application) About(section string, items []foundation.AboutItem) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort functions, no changes.

path = append([]string{support.RelativePath, "resources"}, path...)
return r.absPath(path...)
}
func (r *Application) Run(modules ...foundation.Module) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new function.

}

func TestApplicationTestSuite(t *testing.T) {
assert.Nil(t, file.PutContent(support.EnvFilePath, "APP_KEY=12345678901234567890123456789012"))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no effect before, exit directly when registering the config service provider.

image

@hwbrzzl hwbrzzl marked this pull request as ready for review October 12, 2025 04:41
Copilot AI review requested due to automatic review settings October 12, 2025 04:41
@hwbrzzl hwbrzzl requested a review from a team as a code owner October 12, 2025 04:41
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds a new Run function to the Application interface that optimizes the app running process by launching Route, Grpc, Queue, and Schedule modules concurrently. It also introduces a Context function to handle graceful shutdown via signal listening.

  • Adds Run(modules ...foundation.Module) method to automatically start specified modules or auto-detect based on configuration
  • Adds Context() method to expose the application's context for shutdown coordination
  • Implements graceful shutdown handling with signal capture and context cancellation

Reviewed Changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
contracts/foundation/application.go Defines Module type and adds Context/Run method signatures to Application interface
foundation/application.go Implements new Run and Context methods with module constants and graceful shutdown logic
foundation/application_test.go Adds comprehensive test coverage for the new Run functionality with mocked dependencies
foundation/.env Adds test environment file with APP_KEY for testing
mocks/foundation/Application.go Updates mock implementation with new Context and Run method stubs

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@hwbrzzl
Copy link
Contributor Author

hwbrzzl commented Oct 12, 2025

Hey @goravel/core-developers Could you help confirm this and another PRs? The initiation framework process is changed in these two PRs totally. We can have a discussion on this.

@@ -1,4 +1,3 @@
.env
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we removing it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a .env file for testing (foundation/.env), it's hard to test the code if no .env due to the init function.

Copy link
Member

@krishankumar01 krishankumar01 Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can export the APP_KEY as a GitHub variable, and it can then be exported into the terminal?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a .env file for testing (foundation/.env), it's hard to test the code if no .env due to the init function.

Perhaps consider changing .env to /.env in the .gitignore file to ignore only the .env file in the root directory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can export the APP_KEY as a GitHub variable, and it can then be exported into the terminal?

This way can't test the foundation package locally.

Perhaps consider changing .env to /.env in the .gitignore file to ignore only the .env file in the root directory.

It's the goravel/framework repo, so it doesn't matter .env or /.env.

return r.absPath(path...)
}
func (r *Application) Run(modules ...foundation.Module) {
signal.Notify(r.quit, syscall.SIGINT, syscall.SIGTERM)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use signal.NotifyContext instead of keeping r.quit and r.cancel as a struct-level field.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

modules = append(modules, Queue)
}

schedule := r.MakeSchedule()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we starting scheduling by default? Shouldn’t we enable only the bare minimum here?

Copy link
Contributor Author

@hwbrzzl hwbrzzl Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the schedule will only be run when schedule != nil && len(schedule.Events()) > 0.

artisanFacade.Register(commands)
}

func (r *Application) runRoute() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for runRoute, runGrpc, runQueue, and runSchedule looks similar. Maybe we can create a reusable method that just takes the Run and Shutdown functions as parameters?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit hard to do that. Run functions are different: Run(host ...string) error, Run() error.

Copy link
Member

@krishankumar01 krishankumar01 Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that’s why you shouldn’t call it directly instead, wrap it(but not sure if it’s worth it). (though it’s just a subset of @almas-x ’s suggestion)

func Run() error {
    return route.Run()
}


path = append([]string{support.RelativePath, defaultPath}, path...)
return r.absPath(path...)
for _, module := range modules {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can modulerunner be mapped in a map instead of using a switch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users can customize the modules, so it's harder to build the map.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I’m just talking about the switch case instead of using a switch, we can map each module to its corresponding run method, then resolve the run method based on the modules the user has configured.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, will think about it with @almas-x 's optimization.

*Container

ctx context.Context
cancel context.CancelFunc
Copy link
Member

@krishankumar01 krishankumar01 Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like they(quit and cancel) are not required to be struct level params?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

@almas-x
Copy link
Contributor

almas-x commented Oct 12, 2025

Amazing job👍 This change makes main.go much cleaner. Currently, in our framework, Route, Grpc, Queue, and Schedule are installed as facades on demand. Would it be possible to abstract the Run logic into an interface, where modules could be registered in a way similar to a Service Provider during installation? This approach could also accommodate potential third-party extensions that might require starting services in the main thread in the future.

@hwbrzzl
Copy link
Contributor Author

hwbrzzl commented Oct 12, 2025

@almas-x Good point to provide a way to support the third package. I'll think about it.

@hwbrzzl
Copy link
Contributor Author

hwbrzzl commented Oct 13, 2025

Added a new type Runner interface, PTAL @almas-x @kkumar-gcc .

@hwbrzzl hwbrzzl requested a review from Copilot October 13, 2025 08:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@almas-x
Copy link
Contributor

almas-x commented Oct 13, 2025

Added a new type Runner interface, PTAL @almas-x @kkumar-gcc .

Most of these runners in foundation/runners.go are quite similar, and currently the Application.Run method hardcodes runner for route, grpc, queue, and schedule. Would it be make sense to move the Runner implementation into ServiceProvider? For example, we could define an interface like:

type ServiceProviderWithRunner interface {
    ServiceProvider
    Runner() Runner
}

After loading all ServiceProviders, we could check if any implement Runner, and start their runners if so. This would allow runners to be installed and registered automatically with their respective ServiceProviders.

@hwbrzzl
Copy link
Contributor Author

hwbrzzl commented Oct 13, 2025

@almas-x Good idea, only have one concern: how to stop a runner in this way? According to the current logic, users can customize the runners: Run(runners ...Runner).

@almas-x
Copy link
Contributor

almas-x commented Oct 13, 2025

@almas-x Good idea, only have one concern: how to stop a runner in this way? According to the current logic, users can customize the runners: Run(runners ...Runner).

image

I think the graceful shutdown logic could be moved into Application.Run. Start each runner one by one, and when a quit signal is received, gracefully shut down each runner in order. This centralizes lifecycle management and makes the codebase more maintainable. What are your thoughts on this? @hwbrzzl

@hwbrzzl
Copy link
Contributor Author

hwbrzzl commented Oct 13, 2025

@almas-x Do you mean:

image

@krishankumar01
Copy link
Member

@almas-x Good idea, only have one concern: how to stop a runner in this way? According to the current logic, users can customize the runners: Run(runners ...Runner).

@hwbrzzl I feel runner is more closely related to a facade rather than the service provider itself, since a single provider can expose multiple facades. So instead of making the provider implement runner logic, maybe the provider can just tell us which of its facades implement Runner or Shutdown, and we can handle those automatically.

Something like how we already define Relationship, maybe we can add fields for that:

func (r *ServiceProvider) Relationship() contractsbinding.Relationship {
    bindings := []string{
        contractsbinding.Orm,
        contractsbinding.DB,
        contractsbinding.Schema,
        contractsbinding.Seeder,
    }

    return contractsbinding.Relationship{
        Bindings:     bindings,
        Dependencies: binding.Dependencies(bindings...),
        ProvideFor:   []string{},
        Runner:       []string{},   // facades implementing Runner
        Shutdown:     []string{},   // facades implementing Shutdown
    }
}

This way, the framework can look up which facades actually implement those interfaces and handle start/stop automatically.

@almas-x
Copy link
Contributor

almas-x commented Oct 13, 2025

@almas-x Do you mean:

image

Yep 😉

@almas-x
Copy link
Contributor

almas-x commented Oct 13, 2025

@almas-x Good idea, only have one concern: how to stop a runner in this way? According to the current logic, users can customize the runners: Run(runners ...Runner).

@hwbrzzl I feel runner is more closely related to a facade rather than the service provider itself, since a single provider can expose multiple facades. So instead of making the provider implement runner logic, maybe the provider can just tell us which of its facades implement Runner or Shutdown, and we can handle those automatically.

Something like how we already define Relationship, maybe we can add fields for that:

func (r *ServiceProvider) Relationship() contractsbinding.Relationship {
    bindings := []string{
        contractsbinding.Orm,
        contractsbinding.DB,
        contractsbinding.Schema,
        contractsbinding.Seeder,
    }

    return contractsbinding.Relationship{
        Bindings:     bindings,
        Dependencies: binding.Dependencies(bindings...),
        ProvideFor:   []string{},
        Runner:       []string{},   // facades implementing Runner
        Shutdown:     []string{},   // facades implementing Shutdown
    }
}

This way, the framework can look up which facades actually implement those interfaces and handle start/stop automatically.

That’s a great point, I forgot that a single ServiceProvider can provide multiple facades. However, currently only built-in facades can be installed via artisan package:install for self-service installation, and third-party extensions are not yet supported. So maybe we can change it so that a ServiceProvider exposes multiple runners, for example:

type ServiceProviderWithRunners interface {
    ServiceProvider
    Runners() []Runner
}

@hwbrzzl
Copy link
Contributor Author

hwbrzzl commented Oct 13, 2025

@kkumar-gcc I feel it's a bit strange that Relationship() contains Runners. And my main concern is that a service provider is installed, but user doesn't want to run it. How to achieve this. We may set different conditions for different facades, e.g. for Route, http.host should not be empty, for Grpc, grpc.host should not be empty, etc. It's very complex for user to understand such logic.

In the current logic, if user doesn't pass any runner, we will judge the conditions above. But if user passes runners, the runners will be Run and Shutdown without internal conditions.

image

@almas-x
Copy link
Contributor

almas-x commented Oct 13, 2025

@kkumar-gcc I feel it's a bit strange that Relationship() contains Runners. And my main concern is that a service provider is installed, but user doesn't want to run it. How to achieve this. We may set different conditions for different facades, e.g. for Route, http.host should not be empty, for Grpc, grpc.host should not be empty, etc. It's very complex for user to understand such logic.

In the current logic, if user doesn't pass any runner, we will judge the conditions above. But if user passes runners, the runners will be Run and Shutdown without internal conditions.

image

These checks can be handled inside the implementation of ServiceProviderWithRunners.Runners, for example:

func (r *ServiceProvider) Runners() (runners []foundation.Runner) {
    if configFacade != nil && configFacade.GetString("http.default") != "" {
        runners = append(runners, &RouteRunner{})
    }
    return runners
}

This way, the logic for whether to run a specific runner can be encapsulated within the service provider itself.

@krishankumar01
Copy link
Member

krishankumar01 commented Oct 13, 2025

@kkumar-gcc I feel it's a bit strange that Relationship() contains Runners. And my main concern is that a service provider is installed, but user doesn't want to run it. How to achieve this. We may set different conditions for different facades, e.g. for Route, http.host should not be empty, for Grpc, grpc.host should not be empty, etc. It's very complex for user to understand such logic.

In the current logic, if user doesn't pass any runner, we will judge the conditions above. But if user passes runners, the runners will be Run and Shutdown without internal conditions.

@hwbrzzl Hmm, I agree, it feels odd to put runners in Relationship(). As @almas-x suggested, we can add Runners() []Runner and let each runner's constructor handle any required arguments. If we want users to control which runners start, add an optional ShouldRun() method on the runner so the runner can decide whether to initialize by default or via config. We can also let Runners() return facade/binding names instead of runner instances.

type Runner interface {
    ShouldRun() bool
    Run() error
    Shutdown() error
}

type ServiceProvider interface {
    Runners() []Runner | []Bindings
}

Example:

type Example struct {
    host string
}

func (r *Example ) ShouldRun() bool {
    return r.host != "" // or any other condition derived from config facade
}

func (r *Example) Run() error {
    return nil
}

func (r *Example ) Shutdown() error {
    return nil
}

@hwbrzzl hwbrzzl changed the title feat: [#783] Add app.Run(...module) function to optimize the app running process feat: [#783] Add app.Run(...runner) function to optimize the app running process Oct 13, 2025
@hwbrzzl
Copy link
Contributor Author

hwbrzzl commented Oct 13, 2025

Updated, PTAL. @goravel/core-developers

Copy link
Member

@krishankumar01 krishankumar01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good 👍, just a couple of questions.

Boot(app Application)
}

type ServiceProviderWithRunners interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn’t these interfaces just extend the original ServiceProvider interface?

type ServiceProviderWithRunners interface {
    ServiceProvider
    // Runners returns the service provider's runners.
    Runners(app Application) []Runner
}

type ServiceProviderWithRelations interface {
    ServiceProvider
    // Relationship returns the service provider's relationship.
    Relationship() binding.Relationship
}

setEnv()
setRootPath()

ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIGTERM is Unix-specific, right? Can we confirm if this will work on Windows?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Windows doesn't support SIGTERM, but there is no error on Windows. The code is fine.

go func() {
<-r.ctx.Done()

for _, runner := range allRunners {
Copy link
Member

@krishankumar01 krishankumar01 Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will graceful shutdown work here (like graceful shutdown of HTTP server)? Shouldn't we wait for all runner to actually Run using WaitGroup or similar?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, and if one runner fails to shut down, other runners will be blocked. I think it's better to split them to multiple goroutines. cc @almas-x

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, and if one runner fails to shut down, other runners will be blocked. I think it's better to split them to multiple goroutines. cc @almas-x

That makes sense, splitting them into multiple goroutines since shutting down each runner usually won’t affect the others.

@hwbrzzl
Copy link
Contributor Author

hwbrzzl commented Oct 15, 2025

Addressed comments, PTAL @goravel/core-developers

Copy link
Member

@krishankumar01 krishankumar01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@hwbrzzl hwbrzzl merged commit d947a84 into master Oct 16, 2025
12 of 14 checks passed
@hwbrzzl hwbrzzl deleted the bowen/#783 branch October 16, 2025 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants