-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC : Signals Support for Sanic Proposal #1630
Comments
I am in favor of this. As a quick reminder, some of these proposed signals will not really be supported in ASGI mode. And, I think we might want to work on some of the naming of them to stay consistent (ie |
In general, I think we need to also keep in mind ASGI lifespans. These really only correspond to |
@ahopkins When I did the initial draft of this, our ASGI support was still underway. I am working on an updated draft related to the ASGI lifespan and tweaking the signals accordingly and possibly altering some of their names to a more generic reusable form across the framework. I will reach out to you via Gitter for a review soon |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If this is incorrect, please respond with an update. Thank you for your contributions. |
/reopen |
I've been thinking about this in relation to Sanic-Plugins-Framework. For me the ultimate goal will be to deprecate the method-wrapping that SPF needs to do in order to add functionality to Sanic. Specifically; when installed SPF wraps and replaces the sanic response handler, the request middleware runner, and the response middleware runner. SPF needs to do this in order to provide the following features:
I think the first item and last item can proably be covered by the |
@ashleysommer Thanks for the feedback. I think most of these signals can be easily included with little to no overhead in the current workflow. However, since these signals are only useful in cases when you are using SPF or a plugin built using that, I am wondering if we can think of making this a configurable entity whereby you get to say generate these additional signals and only then will sanic register those signal handler and deliver them. This way, we can avoid the overhead of generating the signals in places where there is no SPF/it's extensions are being used. Would that be a sensible thing to do? |
@harshanarayana |
Agreed. 👍 to handing this at startup. |
Another helper utilities to leverage:
|
(now I noticed we don't have any thread in the forums discussing this, but that's not a problem) Personally, this is something I've been wanting to have on Sanic since day one. I tried to leverage this with my own Frankenstein project based on Sanic - well, it doesn't have any signal support, but does have a component system that is very handy - perhaps another subject for another time 😉 |
@vltr |
@harshanarayana I really liked import asyncio
from sanic import Sanic
from sanic.log import logger
from sanic.response import text
from asyncio_dispatch import Signal
app = Sanic(__name__)
async def my_callback(signal, senders, keys, action):
await asyncio.sleep(3)
if action is not None:
logger.info(
f"my callback called after 3 seconds says action should be: "
f"{action}"
)
@app.listener("before_server_start")
async def setup_signal(app, loop):
app.signal = Signal(loop=loop, action=None)
await app.signal.connect(my_callback)
@app.get("/")
async def test_async(request):
await request.app.signal.send()
return text("hello, world!")
@app.post("/")
async def test_async_post(request):
if "action" in request.json:
await request.app.signal.send(action=request.json.get("action"))
return text("hello, world!")
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, workers=1) Calling using cURL:
Server output:
I think I have this example laying around my computer for 6 months (or more) 😬 |
I'm very much +1 on this idea, definitely something I've found myself struggling with in the past. A few thoughts: Signal Handler Order mattersThere's some nuance in deciding in what order signal handlers get called. If your trying to evaluate an accurate wall time for a request handler you need your Turtles all the way downThis idea taken to its logical maximum can get kind of interesting, like technically request handlers are really just signal handlers that fire on the
To be clear I'm not suggesting we encourage this style of api for our users but there are some interesting possibilities. It be encouraging if this was generic and easy enough to use that the sanic implementation made heavy use of it. Access logs for example is something I'd expect to see as a basic signal handler that we offer as part sanic. Who watches the watchersGoing to a signal heavy implementation does introduce some complexity in understanding the performance of your application. You could logically want to create a signal handler that tries to collect metrics / monitor performance on all signal handlers to add transparency to your app but does that justify the cost of a nested signal handler? Back PressureHowever this gets implemented this it seems like were going to have to keep events around in memory while potentially multiple handlers are working on them (or catching up to them). There has to be some way to control how many events we can queue up for a slow handler and push that back to the things queuing them. VersioningThe context passed to a signal handler is probably going to change over time, should we try to version this in some way so we can maintain backwards compatibility easier? Naming Bikesheds@ahopkins posted the link to the asgi lifespans doc and it looks like their signals have "namespaces" (seems like in convention only), I would be +1 on stealing that idea, something like turning Thanks 😄Huge shout out to @harshanarayana for kicking this off though, really excited to see where this goes! |
How do you plan to run them concurrently? I am concerned because AFAIK you'll have to depend on asyncio for that, and that would require an alternative implementation for trio. |
@abuckenheimer I like the idea of associating a namespace with the signal. It makes it more cleaner and has a definitive association. I'll include that in the changes I am working on. @Tronic Right now, the idea is to schedule them as tasks when sending the events. I still need to explore better options if any. Or follow the serial approach for notification otherwise. I am open to suggestions or additional tooling for the parallel notification mechanism |
Erm ... I'm probably ignorant enough regarding |
Sanic already works with Trio using Hypercorn because Sanic's ASGI mode does not depend on asyncio (except for file responses, which were easily fixed via a few lines of code in compat.py). I'd like to keep that compatibility and preferably not add too much new compatibility code to support it. |
Ok, so, just to clarify things a little bit more: hypothetically, the signals support on Sanic would work on top of ASGI with any level of complexity on top of |
Well, laziness of mine, I did some quick tests using a modified version of the example I shared here previously to run with
The result is already interesting:
Now I don't know if I can start crying already 😅 |
I am pushing this one forward. It can be a really powerful tool for Sanic. Here are some thoughts after playing with the existing PR (and moving it to #1902 so it is on a branch here, and not a fork)
With that said, the API I am proposing would look something like this: @app.on('server.init.before') # would be synonymous with @app.listener("before_server_start")
@app.on('http.request.start') # basically the same as middleware
@app.on('http.request.start', restrictions=restrictions) # where restrictions could limit dispatch on conditions, in this case maybe it is a path or a route name
@app.on('custom.email.send') # custom namespace is for extensibility
# Both forms would be supported. The first probably would be preferred since the routing is explicit and needs and should be faster
app.signals.dispatch('custom', 'email', 'send', extra={}) # extra would get passed and be available on the `signal` instance
app.signals.dispatch('custom.email.send', where=where) # where should match up with the restrictions for limiting |
See also: #2015 (comment) |
This issue has been mentioned on Sanic Community Discussion. There might be relevant details there: https://community.sanicframework.org/t/a-fast-new-router/649/41 |
As mentioned in the linked discussion, I created a super simple proof of concept signal component for Sanic with an API that looks like this: @app.signal("foo.bar.baz")
async def signal_handler(**kwargs):
...
@app.listener("after_server_start")
async def wait_for_event(app, loop):
while True:
print("> waiting")
await app.event("foo.bar.baz")
print("> event found")
@app.get("/")
async def trigger(request):
await app.dispatch("foo.bar.baz")
return response.text("Done.") This is a super simple API implementation. A lot of the heavy work is still being done by the router. I hope to finalize #2010 PR this weekend and pull it out of draft. Therefore, it might be feasible to bump up this feature to include the API for signals in 21.3. If that is the case, I would not transition any existing functionality (middleware, listeners, etc), but just include the new API. @sanic-org/sanic-core-devs Thoughts? |
QQ: |
@jordangarside
New system-level signals that use this new API will start to appear from Sanic version 21.6 and above. |
Proposal : Signals Support for Sanic
Introduction
This proposal defines a brief set of guidelines and behaviors with regards to a new set of entities in the
sanic
modules calledsignals
. These entities will serve as a standalone entities during the lifecycle of asanic
request and will be used as a mechanism to signal a certain kind of even being taken place under the respective core components or anysanic
extension that wants to adopt the signaling behavior.Background
Currently,
sanic
framework provides you a way to bind into therequest
andresponse
lifecycle of an incoming API request via themiddleware
support as@app.middleware("request")
or@app.middleware("response")
. However, this adds a strict binding between your code and how you want to process these events.This can be cumbersome when it comes to adding additional
sanic
extensions that can help in monitoring and metrics tracking purpose. This introduces the following set of drawbacks.decorator
request-response
cycle and this is not always a preferred behavior. All you wanted to do was to monitor the events being triggered in the lifecycle of an event. But tracing behavior should not introduce additional delaySo this proposal suggests a
signal
as a way to bypass this constraint. This will let you decouple the way the signals are handled and that process can reside in it's own context without impacting the business logic.This proposal in it's basic form is a simple
pub-sub
model implementation where the extensions and thesanic
core components will generate certain events and the subscribers to those events can handle them the way they all see fit.Proposal
A signal in it's most simple form is just an
event
like in any of yourpub-sub
models. And the possible behavior is as follow.sanic
core components will be responsible for generating a certain set of events described below.Each of these
signal
will have a concrete context define which instructs when these events will be triggered and what they represent. Any extension/plugin that wants to make you use thesesignal
to perform a certain operation on their own are free to do so at their own discretion.Sanic
will guaranteedeliver-once
behavior of thesesignals
for each and everyone who is subscribing to thissignal
Signal Description
Sanic
application has actually been initialized and starts serving the requestsbefore_server_start
listener eventSanic
application has been initialized and is ready to start serving any and all kind of requestsafter_server_start
listener eventSanic
application context is about to be torn down and the server will stop serving any further API requestsbefore_server_stop
listener eventSanic
application context is torn down and the server has stopped serving the requestsafter_server_stop
listener eventSanic
is about to hand over the request object to the handler responsible for handling that specific requestmiddleware
to therequest
contextSanic
server to be written back into the wire as the data for the requestmiddleware
to theresponse
contextSanic
application has finished returning the response back to the requester. This event can be handy in tracking certain metrics that related to the latency introduced by the network during the response write to the wireSanic
doesn't handle this event in any way and this will be a new introduction as part of this proposal@app.exception
exception handler middlewares thatSanic
provides.Sanic
application context comes across any kind of error/exception that is outside the context of a request lifecycle.Sanic
doesn't handle this event in any way and this will be a new introduction as part of this proposalSanic
framework that can be leveraged by the extension/plugin writer in order to notify other extension/plugin that there is a certain kind of event that has happened in their execution context.Sanic
will define a predefined format that will have to be followed while generating this signal to ensure that the messages are compatible across the board.Sanic
doesn't handle this event in any way and this will be a new introduction as part of this proposalImplementation Details
Helper Utilities
There are few interprocess signal communication/dispatch libraries available that can be leveraged.
Based on some evaluation, we can decide on the best-suited tooling to be used under the hood to bring the
signals
behavior intoSanic
(If none of these existing items suit the needs to what is expected bysanic
we can always fork one of these and extend them to match the need ofsanic
)Implementation Requirements
deliver-once
adherence to each of the generated signalssignals
signal_name.subscribe
to start receiving an eventsignal_name.publish
to generate asignal
into thesanic
infrastructurecallable
function as a callback handler to tackle the eventasync
by nature and there is no guarantee in the order in which thesignal
handlers are processed.signal
event should not be changed in any form or shape.)signal
handler is registered at the runtime by a dynamic section of the code, that handler will only start receiving thosesignals
that occur after the subscriber is initializedTTL
like behavior tagged to any of thesignals
thereby ensuring that we do not oversubscribe to the memory consumption. The message is lost in the void of space and time once it's been delivered to all available subscribers at the time ofsignal
generationinter-process communication
You are, however, free to subscribe to thesignals
and write them to your favoritemessage queue/bus
likeRabbitMQ
orRedis
to be consumed by a wide array of other processes that runs outside ofsanic
application process.Impacted Areas
HttpResponse
)Implementation Guidelines
sanic
namedsignals
to define all possible infrastructure level signalssignal
into thesanic
infrastructure via aregister
apisignals
made available viaregister
api defined in Split exceptions tests #2(localhost:5000/signal/metrics)
to provide the information onsignal
metrics (These metrics are generated based on the information available at that point in time so that they can easily be dumped into a time-series database)To Do
signal
functionalitysignal
functionalityThe text was updated successfully, but these errors were encountered: