Repository: sanic-org/sanic
Pull request #2822 (2023-09-14 05:45:11)

Add support for listener and signal prioritization
The purpose of this PR is to add `priority` to listeners and signals.

Requires: https://github.com/sanic-org/sanic-routing/pull/76
Blocks: https://github.com/sanic-org/sanic-ext/pull/228

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sanic-org/sanic/pull/2822?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) Report
Attention: `3 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`160ec7a`)](https://app.codecov.io/gh/sanic-org/sanic/commit/160ec7ae19ade25bbc8d2417ffd66f78c03b8de5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) 88.217% compared to head [(`e8f2cd8`)](https://app.codecov.io/gh/sanic-org/sanic/pull/2822?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) 88.232%.

| [Files](https://app.codecov.io/gh/sanic-org/sanic/pull/2822?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) | Patch % | Lines |
|---|---|---|
| [sanic/app.py](https://app.codecov.io/gh/sanic-org/sanic/pull/2822?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvYXBwLnB5) | 81.818% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/sanic-org/sanic/pull/2822?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) |
| [sanic/mixins/listeners.py](https://app.codecov.io/gh/sanic-org/sanic/pull/2822?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvbWl4aW5zL2xpc3RlbmVycy5weQ==) | 94.736% | [1 Missing :warning: ](https://app.codecov.io/gh/sanic-org/sanic/pull/2822?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) |

<details><summary>Additional details and impacted files</summary>


```diff
@@              Coverage Diff              @@
##              main     #2822       +/-   ##
=============================================
+ Coverage   88.217%   88.232%   +0.014%     
=============================================
  Files           93        93               
  Lines         7282      7291        +9     
  Branches      1251      1253        +2     
=============================================
+ Hits          6424      6433        +9     
+ Misses         600       599        -1     
- Partials       258       259        +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sanic-org/sanic/pull/2822?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).


Patch:
diff --git a/guide/content/en/guide/basics/handlers.md b/guide/content/en/guide/basics/handlers.md
index 0ea12af1..0f66d0bd 100644
--- a/guide/content/en/guide/basics/handlers.md
+++ b/guide/content/en/guide/basics/handlers.md
@@ -180,6 +180,10 @@ All handlers are named automatically. This is useful for debugging, and for gene
 .. column::
 
     ```python
+    # Two handlers, same function,
+    # different names:
+    # - "foo_arg"
+    # - "foo"
     @app.get("/foo/<arg>", name="foo_arg")
     @app.get("/foo")
     async def foo(request, arg=None):
diff --git a/guide/content/en/guide/basics/listeners.md b/guide/content/en/guide/basics/listeners.md
index 4ff7361a..3e600ad2 100644
--- a/guide/content/en/guide/basics/listeners.md
+++ b/guide/content/en/guide/basics/listeners.md
@@ -79,9 +79,13 @@ async def reload_start(*_):
 @app.main_process_start
 async def main_start(*_):
     print(">>>>>> main_start <<<<<<")
+	
+@app.before_server_start
+async def before_start(*_):
+	print(">>>>>> before_start <<<<<<")
 ```
 
-If this application were run with auto-reload turned on, the `reload_start` function would be called once. This is contrasted with `main_start`, which would be run every time a file is save and the reloader restarts the applicaition process.
+If this application were run with auto-reload turned on, the `reload_start` function would be called once when the reloader process starts. The `main_start` function would also be called once when the main process starts. **HOWEVER**, the `before_start` function would be called once for each worker process that is started, and subsequently every time that a file is saved and the worker is restarted.
 
 ## Attaching a listener
 
@@ -232,6 +236,65 @@ Given the following setup, we should expect to see this in the console if we run
 
     The practical result of this is that if the first listener in `before_server_start` handler setups a database connection, listeners that are registered after it can rely upon that connection being alive both when they are started and stopped.
 
+### Priority
+
+.. new:: v23.12
+
+    In v23.12, the `priority` keyword argument was added to listeners. This allows for fine-tuning the order of execution of listeners. The default priority is `0`. Listeners with a higher priority will be executed first. Listeners with the same priority will be executed in the order they were registered. Furthermore, listeners attached to the `app` instance will be executed before listeners attached to a `Blueprint` instance.
+	
+Overall the rules for deciding the order of execution are as follows:
+
+1. Priority in descending order
+2. Application listeners before Blueprint listeners
+3. Registration order
+
+.. column::
+
+    As an example, consider the following, which will print:
+
+    ```bash
+    third
+    bp_third
+    second
+    bp_second
+    first
+    fourth
+    bp_first
+    ```
+
+.. column::
+
+    ```python
+    @app.before_server_start
+    async def first(app):
+        print("first")
+
+    @app.listener("before_server_start", priority=2)
+    async def second(app):
+        print("second")
+
+    @app.before_server_start(priority=3)
+    async def third(app):
+        print("third")
+
+    @bp.before_server_start
+    async def bp_first(app):
+        print("bp_first")
+
+    @bp.listener("before_server_start", priority=2)
+    async def bp_second(app):
+        print("bp_second")
+
+    @bp.before_server_start(priority=3)
+    async def bp_third(app):
+        print("bp_third")
+
+    @app.before_server_start
+    async def fourth(app):
+        print("fourth")
+
+    app.blueprint(bp)
+    ```
 
 ## ASGI Mode
 
diff --git a/guide/public/assets/style.css b/guide/public/assets/style.css
index 807107f5..80ecad00 100644
--- a/guide/public/assets/style.css
+++ b/guide/public/assets/style.css
@@ -12795,6 +12795,10 @@ p + p {
   .section {
     padding: 3rem 0rem; } }
 
+@media screen and (min-width: 1216px) {
+  .section {
+    padding: 3rem 0rem; } }
+
 .footer {
   margin-bottom: 4rem; }
   .footer a[target=_blank]::after {
diff --git a/guide/style/general.scss b/guide/style/general.scss
index 73d7453e..3feb38fb 100644
--- a/guide/style/general.scss
+++ b/guide/style/general.scss
@@ -68,3 +68,8 @@ p + p { margin-top: 1rem; }
 	padding: 3rem 0rem;
     }
 }
+@media screen and (min-width: $widescreen) {
+    .section {
+	padding: 3rem 0rem;
+    }
+}
diff --git a/guide/webapp/display/markdown.py b/guide/webapp/display/markdown.py
index 05d2c2e4..c7074d9c 100644
--- a/guide/webapp/display/markdown.py
+++ b/guide/webapp/display/markdown.py
@@ -38,9 +38,9 @@ class DocsRenderer(HTMLRenderer):
                     class_="code-block__copy",
                     onclick="copyCode(this)",
                 ):
-                    builder.div(
-                        class_="code-block__rectangle code-block__filled"
-                    ).div(class_="code-block__rectangle code-block__outlined")
+                    builder.div(class_="code-block__rectangle code-block__filled").div(
+                        class_="code-block__rectangle code-block__outlined"
+                    )
             else:
                 builder.pre(E.code(escape(code)))
         return str(builder)
@@ -48,16 +48,12 @@ class DocsRenderer(HTMLRenderer):
     def heading(self, text: str, level: int, **attrs) -> str:
         ident = slugify(text)
         if level > 1:
-            text += self._make_tag(
-                "a", {"href": f"#{ident}", "class": "anchor"}, "#"
-            )
+            text += self._make_tag("a", {"href": f"#{ident}", "class": "anchor"}, "#")
         return self._make_tag(
             f"h{level}",
             {
                 "id": ident,
-                "class": (
-                    f"is-size-{level}-desktop " f"is-size-{level+2}-touch"
-                ),
+                "class": (f"is-size-{level}-desktop " f"is-size-{level+2}-touch"),
             },
             text,
         )
@@ -122,9 +118,7 @@ class DocsRenderer(HTMLRenderer):
     def _make_tag(
         self, tag: str, attributes: dict[str, str], text: str | None = None
     ) -> str:
-        attrs = " ".join(
-            f'{key}="{value}"' for key, value in attributes.items()
-        )
+        attrs = " ".join(f'{key}="{value}"' for key, value in attributes.items())
         if text is None:
             return f"<{tag} {attrs} />"
         return f"<{tag} {attrs}>{text}</{tag}>"
diff --git a/guide/webapp/display/page/docobject.py b/guide/webapp/display/page/docobject.py
index a543ac72..c316c388 100644
--- a/guide/webapp/display/page/docobject.py
+++ b/guide/webapp/display/page/docobject.py
@@ -116,9 +116,12 @@ def organize_docobjects(package_name: str) -> dict[str, str]:
         page_registry[ref].append(module)
         page_content[f"/api/{ref}.md"] += str(builder)
     for ref, objects in page_registry.items():
-        page_content[f"/api/{ref}.md"] = _table_of_contents(objects) + page_content[f"/api/{ref}.md"]
+        page_content[f"/api/{ref}.md"] = (
+            _table_of_contents(objects) + page_content[f"/api/{ref}.md"]
+        )
     return page_content
 
+
 def _table_of_contents(objects: list[str]) -> str:
     builder = Builder(name="Partial")
     with builder.div(class_="table-of-contents"):
@@ -126,7 +129,8 @@ def _table_of_contents(objects: list[str]) -> str:
         for obj in objects:
             module, name = obj.rsplit(".", 1)
             builder.a(
-                E.strong(name), E.small(module),
+                E.strong(name),
+                E.small(module),
                 href=f"#{slugify(obj.replace('.', '-'))}",
                 class_="table-of-contents-item",
             )
@@ -137,9 +141,7 @@ def _extract_docobjects(package_name: str) -> dict[str, DocObject]:
     docstrings = {}
     package = importlib.import_module(package_name)
 
-    for _, name, _ in pkgutil.walk_packages(
-        package.__path__, package_name + "."
-    ):
+    for _, name, _ in pkgutil.walk_packages(package.__path__, package_name + "."):
         module = importlib.import_module(name)
         for obj_name, obj in inspect.getmembers(module):
             if (
@@ -173,9 +175,7 @@ def _docobject_to_html(
 ) -> None:
     anchor_id = slugify(docobject.full_name.replace(".", "-"))
     anchor = E.a("#", class_="anchor", href=f"#{anchor_id}")
-    class_name, heading = _define_heading_and_class(
-        docobject, anchor, as_method
-    )
+    class_name, heading = _define_heading_and_class(docobject, anchor, as_method)
 
     with builder.div(class_=class_name):
         builder(heading)
@@ -229,9 +229,7 @@ def _docobject_to_html(
 
         if docobject.docstring.params:
             with builder.div(class_="box mt-5"):
-                builder.h5(
-                    "Parameters", class_="is-size-5 has-text-weight-bold"
-                )
+                builder.h5("Parameters", class_="is-size-5 has-text-weight-bold")
                 _render_params(builder, docobject.docstring.params)
 
         if docobject.docstring.returns:
@@ -256,9 +254,7 @@ def _signature_to_html(
     parts = []
     parts.append("<span class='function-signature'>")
     for decorator in decorators:
-        parts.append(
-            f"<span class='function-decorator'>@{decorator}</span><br>"
-        )
+        parts.append(f"<span class='function-decorator'>@{decorator}</span><br>")
     parts.append(
         f"<span class='is-italic'>{object_type}</span> "
         f"<span class='has-text-weight-bold'>{name}</span>("
@@ -272,9 +268,7 @@ def _signature_to_html(
         annotation = ""
         if param.annotation != inspect.Parameter.empty:
             annotation = escape(str(param.annotation))
-            parts.append(
-                f": <span class='param-annotation'>{annotation}</span>"
-            )
+            parts.append(f": <span class='param-annotation'>{annotation}</span>")
         if param.default != inspect.Parameter.empty:
             default = escape(str(param.default))
             if annotation == "str":
@@ -285,9 +279,7 @@ def _signature_to_html(
     parts.append(")")
     if signature.return_annotation != inspect.Signature.empty:
         return_annotation = escape(str(signature.return_annotation))
-        parts.append(
-            f": -> <span class='return-annotation'>{return_annotation}</span>"
-        )
+        parts.append(f": -> <span class='return-annotation'>{return_annotation}</span>")
     parts.append("</span>")
     return "".join(parts)
 
@@ -328,8 +320,7 @@ def _render_params(builder: Builder, params: list[DocstringParam]) -> None:
                     E.span(
                         param.type_name,
                         class_=(
-                            "has-text-weight-normal has-text-purple "
-                            "is-size-7 ml-2"
+                            "has-text-weight-normal has-text-purple " "is-size-7 ml-2"
                         ),
                     ),
                 ]
@@ -338,10 +329,7 @@ def _render_params(builder: Builder, params: list[DocstringParam]) -> None:
             builder.dd(
                 HTML(
                     render_markdown(
-                        param.description
-                        or param.arg_name
-                        or param.type_name
-                        or ""
+                        param.description or param.arg_name or param.type_name or ""
                     )
                 )
             )
@@ -354,11 +342,7 @@ def _render_raises(builder: Builder, raises: list[DocstringRaises]) -> None:
             with builder.dl(class_="mt-2"):
                 builder.dt(raise_.type_name, class_="is-family-monospace")
                 builder.dd(
-                    HTML(
-                        render_markdown(
-                            raise_.description or raise_.type_name or ""
-                        )
-                    )
+                    HTML(render_markdown(raise_.description or raise_.type_name or ""))
                 )
 
 
@@ -374,11 +358,7 @@ def _render_returns(builder: Builder, docobject: DocObject) -> None:
         if not return_type or return_type == inspect.Signature.empty:
             return_type = "N/A"
 
-        term = (
-            "Return"
-            if not docobject.docstring.returns.is_generator
-            else "Yields"
-        )
+        term = "Return" if not docobject.docstring.returns.is_generator else "Yields"
         builder.h5(term, class_="is-size-5 has-text-weight-bold")
         with builder.dl(class_="mt-2"):
             builder.dt(return_type, class_="is-family-monospace")
@@ -393,17 +373,11 @@ def _render_returns(builder: Builder, docobject: DocObject) -> None:
             )
 
 
-def _render_examples(
-    builder: Builder, examples: list[DocstringExample]
-) -> None:
+def _render_examples(builder: Builder, examples: list[DocstringExample]) -> None:
     with builder.div(class_="box mt-5"):
         builder.h5("Examples", class_="is-size-5 has-text-weight-bold")
         for example in examples:
             with builder.div(class_="mt-2"):
                 builder(
-                    HTML(
-                        render_markdown(
-                            example.description or example.snippet or ""
-                        )
-                    )
+                    HTML(render_markdown(example.description or example.snippet or ""))
                 )
diff --git a/guide/webapp/display/plugins/inline_directive.py b/guide/webapp/display/plugins/inline_directive.py
index eb7e21f6..ee9c1bac 100644
--- a/guide/webapp/display/plugins/inline_directive.py
+++ b/guide/webapp/display/plugins/inline_directive.py
@@ -4,6 +4,7 @@ from mistune.markdown import Markdown
 
 DIRECTIVE_PATTERN = r":(?:class|func|meth|attr|exc|mod|data|const|obj|keyword|option|cmdoption|envvar):`(?P<ref>sanic\.[^`]+)`"  # noqa: E501
 
+
 def _parse_inline_directive(inline, m: re.Match, state):
     state.append_token(
         {
@@ -14,5 +15,11 @@ def _parse_inline_directive(inline, m: re.Match, state):
     )
     return m.end()
 
+
 def inline_directive(md: Markdown):
-    md.inline.register("inline_directive", DIRECTIVE_PATTERN, _parse_inline_directive, before="escape",)
+    md.inline.register(
+        "inline_directive",
+        DIRECTIVE_PATTERN,
+        _parse_inline_directive,
+        before="escape",
+    )
diff --git a/sanic/app.py b/sanic/app.py
index d0b1c8ff..17fc925e 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -433,7 +433,11 @@ class Sanic(
     # -------------------------------------------------------------------- #
 
     def register_listener(
-        self, listener: ListenerType[SanicVar], event: str
+        self,
+        listener: ListenerType[SanicVar],
+        event: str,
+        *,
+        priority: int = 0,
     ) -> ListenerType[SanicVar]:
         """Register the listener for a given event.
 
@@ -454,10 +458,14 @@ class Sanic(
             raise BadRequest(f"Invalid event: {event}. Use one of: {valid}")
 
         if "." in _event:
-            self.signal(_event.value)(
+            self.signal(_event.value, priority=priority)(
                 partial(self._listener, listener=listener)
             )
         else:
+            if priority:
+                error_logger.warning(
+                    f"Priority is not supported for {_event.value}"
+                )
             self.listeners[_event.value].append(listener)
 
         return listener
@@ -581,7 +589,9 @@ class Sanic(
         return handler.handler
 
     def _apply_listener(self, listener: FutureListener):
-        return self.register_listener(listener.listener, listener.event)
+        return self.register_listener(
+            listener.listener, listener.event, priority=listener.priority
+        )
 
     def _apply_route(
         self, route: FutureRoute, overwrite: bool = False
@@ -633,7 +643,13 @@ class Sanic(
 
     def _apply_signal(self, signal: FutureSignal) -> Signal:
         with self.amend():
-            return self.signal_router.add(*signal)
+            return self.signal_router.add(
+                handler=signal.handler,
+                event=signal.event,
+                condition=signal.condition,
+                exclusive=signal.exclusive,
+                priority=signal.priority,
+            )
 
     @overload
     def dispatch(
@@ -771,16 +787,16 @@ class Sanic(
         """
 
         waiter = self.signal_router.get_waiter(event, condition, exclusive)
+
+        if not waiter and self.config.EVENT_AUTOREGISTER:
+            self.signal_router.reset()
+            self.add_signal(None, event)
+            waiter = self.signal_router.get_waiter(event, condition, exclusive)
+            self.signal_router.finalize()
+
         if not waiter:
-            if self.config.EVENT_AUTOREGISTER:
-                self.signal_router.reset()
-                self.add_signal(None, event)
-                waiter = self.signal_router.get_waiter(
-                    event, condition, exclusive
-                )
-                self.signal_router.finalize()
-            else:
-                raise NotFound("Could not find signal %s" % event)
+            raise NotFound(f"Could not find signal {event}")
+
         return await wait_for(waiter.wait(), timeout=timeout)
 
     def report_exception(
diff --git a/sanic/blueprints.py b/sanic/blueprints.py
index bf0635b3..6617d09c 100644
--- a/sanic/blueprints.py
+++ b/sanic/blueprints.py
@@ -32,7 +32,7 @@ from sanic_routing.route import Route
 from sanic.base.root import BaseSanic
 from sanic.exceptions import SanicException
 from sanic.helpers import Default, _default
-from sanic.models.futures import FutureRoute, FutureStatic
+from sanic.models.futures import FutureRoute, FutureSignal, FutureStatic
 from sanic.models.handler_types import (
     ListenerType,
     MiddlewareType,
@@ -479,7 +479,15 @@ class Blueprint(BaseSanic):
                 continue
             future.condition.update({"__blueprint__": self.name})
             # Force exclusive to be False
-            app._apply_signal(tuple((*future[:-1], False)))
+            app._apply_signal(
+                FutureSignal(
+                    future.handler,
+                    future.event,
+                    future.condition,
+                    False,
+                    future.priority,
+                )
+            )
 
         self.routes += [route for route in routes if isinstance(route, Route)]
         self.websocket_routes += [
diff --git a/sanic/mixins/listeners.py b/sanic/mixins/listeners.py
index 6b547ca9..c7926d60 100644
--- a/sanic/mixins/listeners.py
+++ b/sanic/mixins/listeners.py
@@ -1,6 +1,6 @@
 from enum import Enum, auto
 from functools import partial
-from typing import Callable, List, Optional, Union, overload
+from typing import Callable, List, Optional, Union, cast, overload
 
 from sanic.base.meta import SanicMeta
 from sanic.exceptions import BadRequest
@@ -38,6 +38,8 @@ class ListenerMixin(metaclass=SanicMeta):
         listener_or_event: ListenerType[Sanic],
         event_or_none: str,
         apply: bool = ...,
+        *,
+        priority: int = 0,
     ) -> ListenerType[Sanic]:
         ...
 
@@ -47,6 +49,8 @@ class ListenerMixin(metaclass=SanicMeta):
         listener_or_event: str,
         event_or_none: None = ...,
         apply: bool = ...,
+        *,
+        priority: int = 0,
     ) -> Callable[[ListenerType[Sanic]], ListenerType[Sanic]]:
         ...
 
@@ -55,6 +59,8 @@ class ListenerMixin(metaclass=SanicMeta):
         listener_or_event: Union[ListenerType[Sanic], str],
         event_or_none: Optional[str] = None,
         apply: bool = True,
+        *,
+        priority: int = 0,
     ) -> Union[
         ListenerType[Sanic],
         Callable[[ListenerType[Sanic]], ListenerType[Sanic]],
@@ -81,6 +87,7 @@ class ListenerMixin(metaclass=SanicMeta):
             listener_or_event (Union[ListenerType[Sanic], str]): A listener function or an event name.
             event_or_none (Optional[str]): The event name to listen for if `listener_or_event` is a function. Defaults to `None`.
             apply (bool): Whether to apply the listener immediately. Defaults to `True`.
+            priority (int): The priority of the listener. Defaults to `0`.
 
         Returns:
             Union[ListenerType[Sanic], Callable[[ListenerType[Sanic]], ListenerType[Sanic]]]: The listener or a callable that takes a listener.
@@ -96,7 +103,7 @@ class ListenerMixin(metaclass=SanicMeta):
         """  # noqa: E501
 
         def register_listener(
-            listener: ListenerType[Sanic], event: str
+            listener: ListenerType[Sanic], event: str, priority: int = 0
         ) -> ListenerType[Sanic]:
             """A helper function to register a listener for an event.
 
@@ -112,7 +119,7 @@ class ListenerMixin(metaclass=SanicMeta):
             """
             nonlocal apply
 
-            future_listener = FutureListener(listener, event)
+            future_listener = FutureListener(listener, event, priority)
             self._future_listeners.append(future_listener)
             if apply:
                 self._apply_listener(future_listener)
@@ -123,12 +130,29 @@ class ListenerMixin(metaclass=SanicMeta):
                 raise BadRequest(
                     "Invalid event registration: Missing event name."
                 )
-            return register_listener(listener_or_event, event_or_none)
+            return register_listener(
+                listener_or_event, event_or_none, priority
+            )
         else:
-            return partial(register_listener, event=listener_or_event)
+            return partial(
+                register_listener, event=listener_or_event, priority=priority
+            )
+
+    def _setup_listener(
+        self,
+        listener: Optional[ListenerType[Sanic]],
+        event: str,
+        priority: int,
+    ) -> ListenerType[Sanic]:
+        if listener is not None:
+            return self.listener(listener, event, priority=priority)
+        return cast(
+            ListenerType[Sanic],
+            partial(self.listener, event_or_none=event, priority=priority),
+        )
 
     def main_process_start(
-        self, listener: ListenerType[Sanic]
+        self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0
     ) -> ListenerType[Sanic]:
         """Decorator for registering a listener for the main_process_start event.
 
@@ -149,10 +173,10 @@ class ListenerMixin(metaclass=SanicMeta):
                 print("Main process started")
             ```
         """  # noqa: E501
-        return self.listener(listener, "main_process_start")
+        return self._setup_listener(listener, "main_process_start", priority)
 
     def main_process_ready(
-        self, listener: ListenerType[Sanic]
+        self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0
     ) -> ListenerType[Sanic]:
         """Decorator for registering a listener for the main_process_ready event.
 
@@ -174,10 +198,10 @@ class ListenerMixin(metaclass=SanicMeta):
                 print("Main process ready")
             ```
         """  # noqa: E501
-        return self.listener(listener, "main_process_ready")
+        return self._setup_listener(listener, "main_process_ready", priority)
 
     def main_process_stop(
-        self, listener: ListenerType[Sanic]
+        self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0
     ) -> ListenerType[Sanic]:
         """Decorator for registering a listener for the main_process_stop event.
 
@@ -197,10 +221,10 @@ class ListenerMixin(metaclass=SanicMeta):
                 print("Main process stopped")
             ```
         """  # noqa: E501
-        return self.listener(listener, "main_process_stop")
+        return self._setup_listener(listener, "main_process_stop", priority)
 
     def reload_process_start(
-        self, listener: ListenerType[Sanic]
+        self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0
     ) -> ListenerType[Sanic]:
         """Decorator for registering a listener for the reload_process_start event.
 
@@ -220,10 +244,10 @@ class ListenerMixin(metaclass=SanicMeta):
                 print("Reload process started")
             ```
         """  # noqa: E501
-        return self.listener(listener, "reload_process_start")
+        return self._setup_listener(listener, "reload_process_start", priority)
 
     def reload_process_stop(
-        self, listener: ListenerType[Sanic]
+        self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0
     ) -> ListenerType[Sanic]:
         """Decorator for registering a listener for the reload_process_stop event.
 
@@ -243,10 +267,10 @@ class ListenerMixin(metaclass=SanicMeta):
                 print("Reload process stopped")
             ```
         """  # noqa: E501
-        return self.listener(listener, "reload_process_stop")
+        return self._setup_listener(listener, "reload_process_stop", priority)
 
     def before_reload_trigger(
-        self, listener: ListenerType[Sanic]
+        self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0
     ) -> ListenerType[Sanic]:
         """Decorator for registering a listener for the before_reload_trigger event.
 
@@ -267,10 +291,12 @@ class ListenerMixin(metaclass=SanicMeta):
                 print("Before reload trigger")
             ```
         """  # noqa: E501
-        return self.listener(listener, "before_reload_trigger")
+        return self._setup_listener(
+            listener, "before_reload_trigger", priority
+        )
 
     def after_reload_trigger(
-        self, listener: ListenerType[Sanic]
+        self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0
     ) -> ListenerType[Sanic]:
         """Decorator for registering a listener for the after_reload_trigger event.
 
@@ -291,10 +317,13 @@ class ListenerMixin(metaclass=SanicMeta):
                 print("After reload trigger, changed files: ", changed)
             ```
         """  # noqa: E501
-        return self.listener(listener, "after_reload_trigger")
+        return self._setup_listener(listener, "after_reload_trigger", priority)
 
     def before_server_start(
-        self, listener: ListenerType[Sanic]
+        self,
+        listener: Optional[ListenerType[Sanic]] = None,
+        *,
+        priority: int = 0,
     ) -> ListenerType[Sanic]:
         """Decorator for registering a listener for the before_server_start event.
 
@@ -317,10 +346,10 @@ class ListenerMixin(metaclass=SanicMeta):
                 print("Before server start")
             ```
         """  # noqa: E501
-        return self.listener(listener, "before_server_start")
+        return self._setup_listener(listener, "before_server_start", priority)
 
     def after_server_start(
-        self, listener: ListenerType[Sanic]
+        self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0
     ) -> ListenerType[Sanic]:
         """Decorator for registering a listener for the after_server_start event.
 
@@ -347,10 +376,10 @@ class ListenerMixin(metaclass=SanicMeta):
                 print("After server start")
             ```
         """  # noqa: E501
-        return self.listener(listener, "after_server_start")
+        return self._setup_listener(listener, "after_server_start", priority)
 
     def before_server_stop(
-        self, listener: ListenerType[Sanic]
+        self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0
     ) -> ListenerType[Sanic]:
         """Decorator for registering a listener for the before_server_stop event.
 
@@ -374,10 +403,10 @@ class ListenerMixin(metaclass=SanicMeta):
                 print("Before server stop")
             ```
         """  # noqa: E501
-        return self.listener(listener, "before_server_stop")
+        return self._setup_listener(listener, "before_server_stop", priority)
 
     def after_server_stop(
-        self, listener: ListenerType[Sanic]
+        self, listener: Optional[ListenerType[Sanic]], *, priority: int = 0
     ) -> ListenerType[Sanic]:
         """Decorator for registering a listener for the after_server_stop event.
 
@@ -401,4 +430,4 @@ class ListenerMixin(metaclass=SanicMeta):
                 print("After server stop")
             ```
         """  # noqa: E501
-        return self.listener(listener, "after_server_stop")
+        return self._setup_listener(listener, "after_server_stop", priority)
diff --git a/sanic/mixins/signals.py b/sanic/mixins/signals.py
index b6be4611..f5962d93 100644
--- a/sanic/mixins/signals.py
+++ b/sanic/mixins/signals.py
@@ -24,6 +24,7 @@ class SignalMixin(metaclass=SanicMeta):
         apply: bool = True,
         condition: Optional[Dict[str, Any]] = None,
         exclusive: bool = True,
+        priority: int = 0,
     ) -> Callable[[SignalHandler], SignalHandler]:
         """
         For creating a signal handler, used similar to a route handler:
@@ -51,7 +52,11 @@ class SignalMixin(metaclass=SanicMeta):
 
         def decorator(handler: SignalHandler):
             future_signal = FutureSignal(
-                handler, event_value, HashableDict(condition or {}), exclusive
+                handler,
+                event_value,
+                HashableDict(condition or {}),
+                exclusive,
+                priority,
             )
             self._future_signals.add(future_signal)
 
diff --git a/sanic/models/futures.py b/sanic/models/futures.py
index f9e0644c..c47a27e5 100644
--- a/sanic/models/futures.py
+++ b/sanic/models/futures.py
@@ -33,6 +33,7 @@ class FutureRoute(NamedTuple):
 class FutureListener(NamedTuple):
     listener: ListenerType
     event: str
+    priority: int
 
 
 class FutureMiddleware(NamedTuple):
@@ -65,6 +66,7 @@ class FutureSignal(NamedTuple):
     event: str
     condition: Optional[Dict[str, str]]
     exclusive: bool
+    priority: int
 
 
 class FutureRegistry(set):
diff --git a/sanic/router.py b/sanic/router.py
index 4cf66fe2..091172f3 100644
--- a/sanic/router.py
+++ b/sanic/router.py
@@ -8,6 +8,7 @@ from uuid import UUID
 from sanic_routing import BaseRouter
 from sanic_routing.exceptions import NoMethod
 from sanic_routing.exceptions import NotFound as RoutingNotFound
+from sanic_routing.group import RouteGroup
 from sanic_routing.route import Route
 
 from sanic.constants import HTTP_METHODS
@@ -197,7 +198,7 @@ class Router(BaseRouter):
         return {route.parts: route for route in self.routes}
 
     @property
-    def routes_static(self) -> Dict[Tuple[str, ...], Route]:
+    def routes_static(self) -> Dict[Tuple[str, ...], RouteGroup]:
         """Return all static routes in the router.
 
         _In this context "static" routes do not refer to the `app.static()`
@@ -210,7 +211,7 @@ class Router(BaseRouter):
         return self.static_routes
 
     @property
-    def routes_dynamic(self) -> Dict[Tuple[str, ...], Route]:
+    def routes_dynamic(self) -> Dict[Tuple[str, ...], RouteGroup]:
         """Return all dynamic routes in the router.
 
         _Dynamic routes are routes that contain path parameters._
@@ -221,7 +222,7 @@ class Router(BaseRouter):
         return self.dynamic_routes
 
     @property
-    def routes_regex(self) -> Dict[Tuple[str, ...], Route]:
+    def routes_regex(self) -> Dict[Tuple[str, ...], RouteGroup]:
         """Return all regex routes in the router.
 
         _Regex routes are routes that contain path parameters with regex
diff --git a/sanic/signals.py b/sanic/signals.py
index 799d1e91..755e8876 100644
--- a/sanic/signals.py
+++ b/sanic/signals.py
@@ -312,7 +312,7 @@ class SignalRouter(BaseRouter):
         event: Union[str, Enum],
         condition: Optional[Dict[str, Any]] = None,
         exclusive: bool = True,
-    ):
+    ) -> Optional[SignalWaiter]:
         event_definition = self.format_event(event)
         name, trigger, _ = self._get_event_parts(event_definition)
         signal = cast(Signal, self.name_index.get(name))
@@ -349,6 +349,8 @@ class SignalRouter(BaseRouter):
         event: Union[str, Enum],
         condition: Optional[Dict[str, Any]] = None,
         exclusive: bool = True,
+        *,
+        priority: int = 0,
     ) -> Signal:
         event_definition = self.format_event(event)
         name, trigger, event_string = self._get_event_parts(event_definition)
@@ -358,6 +360,7 @@ class SignalRouter(BaseRouter):
             handler,
             name=name,
             append=True,
+            priority=priority,
         )  # type: ignore
 
         signal.ctx.exclusive = exclusive
diff --git a/setup.py b/setup.py
index 63d6d01f..f1ee5581 100644
--- a/setup.py
+++ b/setup.py
@@ -99,7 +99,8 @@ ujson = "ujson>=1.35" + env_dependency
 uvloop = "uvloop>=0.15.0" + env_dependency
 types_ujson = "types-ujson" + env_dependency
 requirements = [
-    "sanic-routing>=23.6.0",
+    # "sanic-routing>=23.6.0",
+    "sanic-routing @ git+https://github.com/sanic-org/sanic-routing.git",
     "httptools>=0.0.10",
     uvloop,
     ujson,


Test patch:
diff --git a/tests/test_server_events.py b/tests/test_server_events.py
index ab0475b6..2ad1d7a7 100644
--- a/tests/test_server_events.py
+++ b/tests/test_server_events.py
@@ -9,6 +9,7 @@ import pytest
 from sanic_testing.testing import HOST, PORT
 
 from sanic.exceptions import BadRequest, SanicException
+from sanic import Blueprint
 
 
 AVAILABLE_LISTENERS = [
@@ -111,9 +112,7 @@ async def test_trigger_before_events_create_server(app):
     async def init_db(app, loop):
         app.ctx.db = MySanicDb()
 
-    srv = await app.create_server(
-        debug=True, return_asyncio_server=True, port=PORT
-    )
+    srv = await app.create_server(debug=True, return_asyncio_server=True, port=PORT)
     await srv.startup()
     await srv.before_start()
 
@@ -170,9 +169,7 @@ def test_create_server_trigger_events(app):
     with closing(socket()) as sock:
         sock.bind(("127.0.0.1", 0))
 
-        serv_coro = app.create_server(
-            return_asyncio_server=True, sock=sock, debug=True
-        )
+        serv_coro = app.create_server(return_asyncio_server=True, sock=sock, debug=True)
         serv_task = asyncio.ensure_future(serv_coro, loop=loop)
         server = loop.run_until_complete(serv_task)
         loop.run_until_complete(server.startup())
@@ -202,9 +199,7 @@ async def test_missing_startup_raises_exception(app):
     async def init_db(app, loop):
         ...
 
-    srv = await app.create_server(
-        debug=True, return_asyncio_server=True, port=PORT
-    )
+    srv = await app.create_server(debug=True, return_asyncio_server=True, port=PORT)
 
     with pytest.raises(SanicException):
         await srv.before_start()
@@ -221,3 +216,51 @@ def test_reload_listeners_attached(app):
 
     assert len(app.listeners.get("reload_process_start")) == 2
     assert len(app.listeners.get("reload_process_stop")) == 2
+
+
+def test_priority_ordering(app):
+    output = []
+    bp = Blueprint("bp")
+
+    @app.before_server_start
+    async def first(app):
+        output.append("first")
+
+    @app.listener("before_server_start", priority=2)
+    async def second(app):
+        output.append("second")
+
+    @app.before_server_start(priority=3)
+    async def third(app):
+        output.append("third")
+
+    @bp.before_server_start
+    async def bp_first(app):
+        output.append("bp_first")
+
+    @bp.listener("before_server_start", priority=2)
+    async def bp_second(app):
+        output.append("bp_second")
+
+    @bp.before_server_start(priority=3)
+    async def bp_third(app):
+        output.append("bp_third")
+
+    @app.before_server_start
+    async def fourth(app):
+        output.append("fourth")
+
+    app.blueprint(bp)
+    start_stop_app(app)
+
+    # The order of the listeners is:
+    # priority descending, app before bp, definition order ascending
+    assert output == [
+        "third",
+        "bp_third",
+        "second",
+        "bp_second",
+        "first",
+        "fourth",
+        "bp_first",
+    ]