-
Notifications
You must be signed in to change notification settings - Fork 260
fix: fixed error handling in asgi application lifespan #2352
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
base: main
Are you sure you want to change the base?
Conversation
When running tests on my computer, I get unrelated errors. I assume these are flaky tests? However, the new tests run perfectly fine. |
@Lancetnik feel free to review whenever you find the time. This should fix any of the issues discussed in the linked issue. Thanks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added tests doesn't proof related changes. All new tests are green with the old code as well
Ah okay, i might understand the problem now. from time import sleep
from faststream.asgi import AsgiFastStream
from faststream.nats.broker import NatsBroker
broker = NatsBroker()
app = AsgiFastStream(broker)
@app.on_startup
async def setup():
raise ValueError()
if __name__ == "__main__":
from starlette.testclient import TestClient
with TestClient(app, base_url="http://localhost:8888") as client:
sleep(100) This leaves me with the question, how I could test this properly. Perhaps, I could add a test case in the CLI test suite which uses uvicorn under the hood? Do you have other ideas? I think the current tests are useless as you mentioned |
Thank you! |
I have added a better test that now fails with the old code and succeeds with the new code. from contextlib import asynccontextmanager
from faststream.asgi import AsgiFastStream
from faststream.nats.broker import NatsBroker
broker = NatsBroker()
@asynccontextmanager
async def lifespan():
raise ValueError("Lifespan error")
yield
app = AsgiFastStream(broker, lifespan=lifespan)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app) |
@Sehat1137 do you know why the test_run test failed? When I am running this locally (and devcontainer), all tests pass Edit: Is this something that breaks the current behaviour of the ASGI app in your eyes @Lancetnik ? |
Please, rebase your branch |
Hey, I have added the tests in tests/cli/rabbit/test_app.py. I had to call uvicorn directly to make sure I get access to the uvicron server instance to check if the appropiate lifespan hook has been called. Additionally, I had to refactor how tests/cli/test_run.py was setup. Please take a look if you are happy with the result. |
@marcm-ml |
I tried to explain the reason previously here. I think it was not apparent that the broker setup in this specific test was always unsuccessful since the broker startup was handled after asgi startup. My original issue was created exactly due to this behaviour where a broker setup was not successful but the asgi server kept running. Edit: |
@marcm-ml |
@marcm-ml Previously, such behavior was not observed. But this actual only for NatsBroker. You may try to repeat this case from faststream import FastStream
from faststream.nats import NatsBroker # Not ok
from faststream.rabbit import RabbitBroker # ok
from faststream.redis import RedisBroker # ok
from faststream.kafka import KafkaBroker # ok
broker = NatsBroker()
app = FastStream(broker) |
for me every broker is failing. Nats is appears stuck because it has a high reconnect attempt setting. if you set max_reconnect_attempts=1 than nats fails instantly for me. Redis is also stuck for me. Kafka and Rabbit exit instantly. |
Maybe to clarify: The behaviour is expected that ctrl+c does not work in my code but works in main branch. We are now awaiting the startup and not sending it off as a background task using anyio task group. During the startup lifespan, uvicorn traps any signal (like ctrl+c / SIGINT) until startup is done. since the startup is only done after the broker has been setup, we cannot abort the startup. It is unfortunate that nats default reconnect parameter is set as high (60*2s = 2mins), which means u need to wait 2 mins to exit. I argue that this was the reason the current implementation was setup in that way since it appears that the application is unresponsive. I would argue that this is a issue with nats implementation rather than the ASGI one. I think it is up to you guys to decide what kind of behaviour you would want to see here Edit: You can see the same behaviour without using fastream. Run via import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app):
await asyncio.sleep(10) # this is equivalent to nats broker trying to connect
yield
app = FastAPI(lifespan=lifespan) |
@marcm-ml |
I believe the current implementation is fundamentally broken and not how an ASGI application should be started. If you want to retain the current behaviour, you must make sure that the |
@marcm-ml sorry, I didn't investigate for the problem too close before I can't agree, that broker startup error should fails ASGI runtime from faststream.asgi import AsgiFastStream, AsgiResponse, get
from faststream.nats import NatsBroker
@get
async def index(scope):
return AsgiResponse(b"Hello, World!", 200)
broker = NatsBroker()
app = AsgiFastStream(broker, asgi_routes=[("/test", index)]) The following code process But, lifespan errors should fail the app according ASGI specification. I suggest to split your Issue into two:
|
It is not correct. We follow semantic versioning, so in our case it means that versions under 1.0.0 has a majority in 0.X release. Therefore we can't break compatibility in patches like Additionally, we have many users and a large community, so we cannot break compatibility without a deprecation phase. Any breaking changes must have the following lifespan:
|
The problem is, that we call https://github.com/ag2ai/faststream/blob/main/faststream/_internal/application.py#L168-L180 And we move this call to asyncio task in ASGI – https://github.com/ag2ai/faststream/blob/main/faststream/asgi/app.py#L189 This reason, the following code doesn't fail ASGI application startup @app.on_startup
async def setup():
raise ValueError() To fix it we should separate |
Point 4. of the semantic versioning spec states that version 0.x is initial development and subject to any change. |
Yeah, I had read the specification. Anyway, we conclude to use minor patches as major until 1.0 release. So, we introduce any minor features & patches as patch releases |
I am confused that broker startup should not be part of the ASGI startup. But if you do not want to change that behaviour, it is fine with me. |
Thank you for your time and effort in investigating the issue. You have done a great job, and I apologize for not clarifying things sooner. I attempted to clarify my thoughts and concerns, but we can still continue to discuss them. This is just my perspective. In any case, the original issue was regarding @app.on_startup
async def setup():
raise ValueError() which does not fail the application. The issue still exists and needs to be addressed, so I do not see any reason for closing the issue at this time. But, if you don't want to work on it, you can close the PR. Thank you and sorry one more time. |
I can look into it in a few days again. Will let you know 👍 |
@marcm-ml Hi, any updates? |
Description
Fixes exception handling in ASGI application startup. The previous implementation had the side-effect that exception within startup (i.e. broker failure), would cause the underlying ASGI server to get stuck waiting for either the "lifespan.startup.complete" or "lifespan.startup.failed" event. In this state, the ASGI server would ignore all signals (like SIGINT) and becomes unresponsive.
Fixes #2305
Type of change
Please delete options that are not relevant.
Checklist
scripts/lint.sh
shows no errors)scripts/test-cov.sh
scripts/static-analysis.sh