Signals integrations

For those that have been following the discussion, since the introduction of Signals in 21.3, the intention has been to use them to replace and optimize the request response cycle using them. In addition to “replacing” listeners and middleware, the idea is to give an even greater depth into the lifecycle of the server and allowing for more customization.

In short, we are going to litter the code with:

await app.dispatch("foo.bar.baz")

Doing this alone would be terrible for performance. There is no need to do more heaviy lifting than required. Therefore, I started to work on both the signal integration, and a startup time parser that would “rewrite” parts of Sanic based upon the registered signals.

Before I get too far into the weeds, I want to pitch the idea and see if there is any more useful feedback.


The startup process would look something like this:

  1. Identify routes, build router
  2. Identify registered signals that are within a registered Sanic namespace
  3. DIspatch any early cycle signals
  4. Scan a group of predetermined functions that can be “optimized” and either inject or remove code required to if a signal hasbeen registered
  5. Look thru middleware and create one or more call chains based upon the routes they apply to and rewrite the handlers to have the dispatchers for the appropriate middleware
  6. Continue as normal dispatching signals instead of events

One change this will require to the existing API is to allow for signals to be awaited in the current task and not pushed off to the background.

Please feel fre to let me know your thoughts. I will push my working branch to the repo later this week.

My current thought for how to accomplish this is to use parseable comments in the code. When a comment is found (at startup) we inject the code snippet into the function. I think this would be easier than parsing the source with AST and looking for the dispatch and falling into the same place backwards. Having the explicit ability to foretell what is optional provides a more consistent result with less edge cases (getattr can be a real nightmare).

What I am thinking is something like this:

def some_func():
    do_something()

    # ODE: foo.bar.baz app.dispatch {}

    do_more()

In this example ODE stands for “optional dispatch event”. We could similarly have other flags. But, in this context this tells the optimizer three things:

  1. Only inject this if there is a registered foo.bar.baz signal handler
  2. In this function, the dispatch function can be achieved using app.dispatch
  3. Additional args to pass are {}

This is a simple example, but so far it works and is easy to implement without being too invasive and suffering from the problems of implicit analysis.

Thoughts? Room for improvement?

Here is a rough idea of what I am talking about:

from inspect import getsourcelines


registry = ("one.two.three",)


class Optimize(type):
    def __new__(cls, name, bases, attrs, **kwargs):
        global registry

        gen_class = super().__new__(cls, name, bases, attrs, **kwargs)

        try:
            funcs = attrs["__optimize__"]
        except KeyError:
            raise Exception(
                f"{cls} cannot be optimized without __optimize__ property"
            )

        for func_name in funcs:
            func = getattr(gen_class, func_name)
            lines, _ = getsourcelines(func)
            offset = len(lines[0]) - len(lines[0].lstrip(" "))
            for idx, line in enumerate(lines):
                line = line[offset:]
                if "# ODE:" in line:
                    line_offset = line.index("# ODE:")
                    indent = line_offset * " "
                    event, caller, extra = line[line_offset + 7 :].split(" ")
                    if event in registry:
                        line = f'{indent}{caller}("{event}")'
                lines[idx] = line.replace("\n", "")
            src = "\n".join(lines)
            print(src)

            try:
                compiled_src = compile(
                    src,
                    "",
                    "exec",
                )
            except SyntaxError as se:
                syntax_error = (
                    f"Line {se.lineno}: {se.msg}\n{se.text}"
                    f"{' '*max(0,int(se.offset or 0)-1) + '^'}"
                )
                print(syntax_error)

            ctx = {}
            exec(compiled_src, None, ctx)
            setattr(gen_class, func_name, ctx[func_name])

        return gen_class


class Foo(metaclass=Optimize):
    __optimize__ = ("bar",)

    def dispatch(self, event):
        print(event)

    def bar(self):
        print("one")
        # ODE: one.two.three self.dispatch {}
        print("two")


foo = Foo()
foo.bar()

Output:

def bar(self):
    print("one")
    self.dispatch("one.two.three")
    print("two")
one
one.two.three
two

See Signal integration by ahopkins · Pull Request #2141 · sanic-org/sanic · GitHub