Transaction callbacks and middlewares

In most cases, the handler is executed by calling one of the Application methods: call(), execute(), or publish(), which creates the transaction context under the hood.

When call() or similar method is executed, the lifecycle of handler execution is as following:

  1. on_enter_transaction_context callback is invoked

  2. transaction_middleware functions, wrapping the handler are invoked

  3. handler function is invoked

  4. on_exit_transaction_context callback is invoked

The application can be configured to use any of the callbacks by using decorators:

from typing import Optional, Callable
from lato import Application, TransactionContext


app = Application()

@app.on_enter_transaction_context
def on_enter_transaction_context(ctx: TransactionContext):
    print("starting transaction")

@app.on_exit_transaction_context
def on_exit_transaction_context(ctx: TransactionContext,
                                exception: Optional[Exception]=None):
    print("exiting transaction context")

@app.transaction_middleware
def middleware1(ctx: TransactionContext, call_next: Callable):
    print("entering middleware1")
    result = call_next()  # will call middleware2
    print("exiting middleware1")
    return result

@app.transaction_middleware
def middleware2(ctx: TransactionContext, call_next: Callable):
    print("entering middleware2")
    result = call_next()  # will call the handler
    print("exiting middleware2")
    return f"[{result}]"

def to_uppercase(s):
    print("calling handler")
    return s.upper()


print(app.call(to_uppercase, "foo"))

This will generate the output:

starting transaction
entering middleware1
entering middleware2
calling handler
exiting middleware2
exiting middleware1
exiting transaction context
[FOO]

Any of the callbacks is optional, and there can be multiple transaction_middleware callbacks. on_enter_transaction_context is a good place to set up the transaction level dependencies, i.e. the dependencies that change with every transaction - correlation id, current time, database session, etc. call_next is a functools.partial object, so you can inspect is arguments using call_next.args and call_next.keywords.