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

App.Run: Respect Shutdowner ExitCode #1075

Merged
merged 3 commits into from
Apr 29, 2023

Conversation

abhinav
Copy link
Collaborator

@abhinav abhinav commented Apr 29, 2023

We added support for changing the exit code for a Shutdowner with the
ExitCode option in #989, but this was somehow not respected by App.Run.

This changes App.Run to use the same underlying machinery (Wait()) to
decide on the exit code to use.

To test this, we add a new internal/e2e submodule that will hold full,
end-to-end integration tests.
These can be full Fx applications that we run tests against.
This is a submodule so that it can have dependencies that are not
desirable as direct dependencies of Fx,
and it's inside the internal/ directory so that it can consume
Fx-internal packages (like testutil).

The included regression test verifies the behavior described in #1074.
An Fx program using App.Run, and shut down with Shutdowner.Shutdown
and an explicit exit code, does not exit with the requested exit code.
Failure before the fix:

% go test
--- FAIL: TestShutdownExitCode (0.01s)
    writer.go:40: [Fx] PROVIDE  fx.Lifecycle <= go.uber.org/fx.New.func1()
    writer.go:40: [Fx] PROVIDE  fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
    writer.go:40: [Fx] PROVIDE  fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
    writer.go:40: [Fx] INVOKE           go.uber.org/fx/internal/e2e/shutdowner_run_exitcode.main.func1()
    writer.go:40: [Fx] RUNNING
    writer.go:40: [Fx] TERMINATED
    main_test.go:46:
                Error Trace:    [..]/fx/internal/e2e/shutdowner_run_exitcode/main_test.go:46
                Error:          An error is expected but got nil.
                Test:           TestShutdownExitCode
FAIL
exit status 1
FAIL    go.uber.org/fx/internal/e2e/shutdowner_run_exitcode     0.016s

Resolves #1074


There's a follow up to this: abhinav#1.
It depends on the e2e test machinery, so I'll make a PR out of it once this is merged.

Adds an internal/e2e submodule to place end-to-end tests in.
These can be full-fledged Fx applications that we run tests against.

This is a submodule so that it can have dependencies that are otherwise
not desirable for Fx.
This is inside the internal/ directory so that it can consume
Fx-internal packages (despite being in a separate submodule).
@codecov
Copy link

codecov bot commented Apr 29, 2023

Codecov Report

Merging #1075 (b1fc1c0) into master (590809b) will increase coverage by 0.00%.
The diff coverage is 100.00%.

@@           Coverage Diff           @@
##           master    #1075   +/-   ##
=======================================
  Coverage   98.53%   98.53%           
=======================================
  Files          39       40    +1     
  Lines        2996     3003    +7     
=======================================
+ Hits         2952     2959    +7     
  Misses         37       37           
  Partials        7        7           
Impacted Files Coverage Δ
app.go 97.22% <100.00%> (+<0.01%) ⬆️
internal/e2e/shutdowner_run_exitcode/main.go 100.00% <100.00%> (ø)

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

abhinav added 2 commits April 29, 2023 11:03
Adds a regression test verifying the behavior described in uber-go#1074.
An Fx program using App.Run, and shut down with Shutdowner.Shutdown
and an explcit exit code, does not exit with the requested exit code.

This test fails right now:

```
% go test
--- FAIL: TestShutdownExitCode (0.01s)
    writer.go:40: [Fx] PROVIDE  fx.Lifecycle <= go.uber.org/fx.New.func1()
    writer.go:40: [Fx] PROVIDE  fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
    writer.go:40: [Fx] PROVIDE  fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
    writer.go:40: [Fx] INVOKE           go.uber.org/fx/internal/e2e/shutdowner_run_exitcode.main.func1()
    writer.go:40: [Fx] RUNNING
    writer.go:40: [Fx] TERMINATED
    main_test.go:46:
                Error Trace:    [..]/fx/internal/e2e/shutdowner_run_exitcode/main_test.go:46
                Error:          An error is expected but got nil.
                Test:           TestShutdownExitCode
FAIL
exit status 1
FAIL    go.uber.org/fx/internal/e2e/shutdowner_run_exitcode     0.016s
```
We added support for changing the exit code for a Shutdowner with the
ExitCode option in uber-go#989, but this was somehow not respected by App.Run.

This changes App.Run to use the same underlying machinery (`Wait()`)
to decide on the exit code to use.

Resolves uber-go#1074
@abhinav abhinav force-pushed the run-shutdown-exitcode branch from 9cc86f4 to b1fc1c0 Compare April 29, 2023 18:03
abhinav added a commit to abhinav/fx that referenced this pull request Apr 29, 2023
Adds a test for calling Shutdown from an fx.Invoke.
This is partially tested in uber-go#1075 already,
but as reported in uber-go#1074, it doesn't work if Start is used.
This adds a test for that case as well.
abhinav added a commit to abhinav/fx that referenced this pull request Apr 29, 2023
App.Start nils out the "last" signal recorded by signalReceivers,
which it otherwise broadcasts to waiters if it was already received.
This is unnecessary especially because there's a discrepancy in behavior
of using App.Start vs App.Run when shutting down from fx.Invoke.

Given a program that calls Shutdown from fx.Invoke,
when we do:

    app := fx.New(...)

The shutdowner has already sent the signal, and signalReceivers has
already recorded it.
At that point, whether we call App.Start or App.Run changes behavior:

- If we call App.Run, that calls App.Done (or App.Wait after uber-go#1075),
  which gives it back a channel that already has the signal filled in.
  It then calls App.Start, waits on the channel--which returns
  immediately--and then calls App.Stop.
- If we call App.Start and App.Wait, on the other hand,
  Start will clear the signal recorded in signalReceivers,
  and then App.Wait will build a channel that will block indefinitely
  because Shutdowner.Shutdown will not be called again.

So even though App.Run() and App.Start()+App.Wait() are meant to be
equivalent, this causes a serious discrepancy in behavior.
It makes sense to resolve this by supporting Shutdown from Invoke.

Refs uber-go#1074
abhinav added a commit to abhinav/fx that referenced this pull request Apr 29, 2023
Adds a test for calling Shutdown from an fx.Invoke.
This is partially tested in uber-go#1075 already,
but as reported in uber-go#1074, it doesn't work if Start is used.
This adds a test for that case as well.
abhinav added a commit to abhinav/fx that referenced this pull request Apr 29, 2023
App.Start nils out the "last" signal recorded by signalReceivers,
which it otherwise broadcasts to waiters if it was already received.
This is unnecessary especially because there's a discrepancy in behavior
of using App.Start vs App.Run when shutting down from fx.Invoke.

Given a program that calls Shutdown from fx.Invoke,
when we do:

    app := fx.New(...)

The shutdowner has already sent the signal, and signalReceivers has
already recorded it.
At that point, whether we call App.Start or App.Run changes behavior:

- If we call App.Run, that calls App.Done (or App.Wait after uber-go#1075),
  which gives it back a channel that already has the signal filled in.
  It then calls App.Start, waits on the channel--which returns
  immediately--and then calls App.Stop.
- If we call App.Start and App.Wait, on the other hand,
  Start will clear the signal recorded in signalReceivers,
  and then App.Wait will build a channel that will block indefinitely
  because Shutdowner.Shutdown will not be called again.

So even though App.Run() and App.Start()+App.Wait() are meant to be
equivalent, this causes a serious discrepancy in behavior.
It makes sense to resolve this by supporting Shutdown from Invoke.

Refs uber-go#1074
Copy link
Contributor

@sywhang sywhang left a comment

Choose a reason for hiding this comment

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

Thanks for fixing this and adding the e2e tests. LGTM

internal/e2e/README.md Show resolved Hide resolved
@sywhang sywhang merged commit 8f5597f into uber-go:master Apr 29, 2023
abhinav added a commit to abhinav/fx that referenced this pull request Apr 29, 2023
Adds a test for calling Shutdown from an fx.Invoke.
This is partially tested in uber-go#1075 already,
but as reported in uber-go#1074, it doesn't work if Start is used.
This adds a test for that case as well.
abhinav added a commit to abhinav/fx that referenced this pull request Apr 29, 2023
App.Start nils out the "last" signal recorded by signalReceivers,
which it otherwise broadcasts to waiters if it was already received.
This is unnecessary especially because there's a discrepancy in behavior
of using App.Start vs App.Run when shutting down from fx.Invoke.

Given a program that calls Shutdown from fx.Invoke,
when we do:

    app := fx.New(...)

The shutdowner has already sent the signal, and signalReceivers has
already recorded it.
At that point, whether we call App.Start or App.Run changes behavior:

- If we call App.Run, that calls App.Done (or App.Wait after uber-go#1075),
  which gives it back a channel that already has the signal filled in.
  It then calls App.Start, waits on the channel--which returns
  immediately--and then calls App.Stop.
- If we call App.Start and App.Wait, on the other hand,
  Start will clear the signal recorded in signalReceivers,
  and then App.Wait will build a channel that will block indefinitely
  because Shutdowner.Shutdown will not be called again.

So even though App.Run() and App.Start()+App.Wait() are meant to be
equivalent, this causes a serious discrepancy in behavior.
It makes sense to resolve this by supporting Shutdown from Invoke.

Refs uber-go#1074
abhinav added a commit to abhinav/fx that referenced this pull request May 6, 2023
Adds a test for calling Shutdown from an fx.Invoke.
This is partially tested in uber-go#1075 already,
but as reported in uber-go#1074, it doesn't work if Start is used.
This adds a test for that case as well.
abhinav added a commit to abhinav/fx that referenced this pull request May 6, 2023
App.Start nils out the "last" signal recorded by signalReceivers,
which it otherwise broadcasts to waiters if it was already received.
This is unnecessary especially because there's a discrepancy in behavior
of using App.Start vs App.Run when shutting down from fx.Invoke.

Given a program that calls Shutdown from fx.Invoke,
when we do:

    app := fx.New(...)

The shutdowner has already sent the signal, and signalReceivers has
already recorded it.
At that point, whether we call App.Start or App.Run changes behavior:

- If we call App.Run, that calls App.Done (or App.Wait after uber-go#1075),
  which gives it back a channel that already has the signal filled in.
  It then calls App.Start, waits on the channel--which returns
  immediately--and then calls App.Stop.
- If we call App.Start and App.Wait, on the other hand,
  Start will clear the signal recorded in signalReceivers,
  and then App.Wait will build a channel that will block indefinitely
  because Shutdowner.Shutdown will not be called again.

So even though App.Run() and App.Start()+App.Wait() are meant to be
equivalent, this causes a serious discrepancy in behavior.
It makes sense to resolve this by supporting Shutdown from Invoke.

Refs uber-go#1074
abhinav added a commit to abhinav/fx that referenced this pull request May 6, 2023
Adds a test for calling Shutdown from an fx.Invoke.
This is partially tested in uber-go#1075 already,
but as reported in uber-go#1074, it doesn't work if Start is used.
This adds a test for that case as well.
abhinav added a commit to abhinav/fx that referenced this pull request May 6, 2023
App.Start nils out the "last" signal recorded by signalReceivers,
which it otherwise broadcasts to waiters if it was already received.
This is unnecessary especially because there's a discrepancy in behavior
of using App.Start vs App.Run when shutting down from fx.Invoke.

Given a program that calls Shutdown from fx.Invoke,
when we do:

    app := fx.New(...)

The shutdowner has already sent the signal, and signalReceivers has
already recorded it.
At that point, whether we call App.Start or App.Run changes behavior:

- If we call App.Run, that calls App.Done (or App.Wait after uber-go#1075),
  which gives it back a channel that already has the signal filled in.
  It then calls App.Start, waits on the channel--which returns
  immediately--and then calls App.Stop.
- If we call App.Start and App.Wait, on the other hand,
  Start will clear the signal recorded in signalReceivers,
  and then App.Wait will build a channel that will block indefinitely
  because Shutdowner.Shutdown will not be called again.

So even though App.Run() and App.Start()+App.Wait() are meant to be
equivalent, this causes a serious discrepancy in behavior.
It makes sense to resolve this by supporting Shutdown from Invoke.

Refs uber-go#1074
sywhang added a commit that referenced this pull request May 8, 2023
Stacked on top of:

- #1081
- #1082

However, since I can't push branches directly to this repository,
this PR shows commits from all PRs.

---

App.Start nils out the "last" signal recorded by signalReceivers,
which it otherwise broadcasts to waiters if it was already received.
This is unnecessary especially because there's a discrepancy in behavior
of using App.Start vs App.Run when shutting down from fx.Invoke.

Given a program that calls Shutdown from fx.Invoke,
when we do:

    app := fx.New(...)

The shutdowner has already sent the signal, and signalReceivers has
already recorded it.
At that point, whether we call App.Start or App.Run changes behavior:

- If we call App.Run, that calls App.Done (or App.Wait after #1075),
  which gives it back a channel that already has the signal filled in.
  It then calls App.Start, waits on the channel--which returns
  immediately--and then calls App.Stop.
- If we call App.Start and App.Wait, on the other hand,
  Start will clear the signal recorded in signalReceivers,
  and then App.Wait will build a channel that will block indefinitely
  because Shutdowner.Shutdown will not be called again.

So even though App.Run() and App.Start()+App.Wait() are meant to be
equivalent, this causes a serious discrepancy in behavior.
It makes sense to resolve this by supporting Shutdown from Invoke.

Refs #1074

---------

Co-authored-by: Sung Yoon Whang <sungyoon@uber.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Terminate my application with exit code
2 participants