Skip to content

Latest commit

 

History

History
206 lines (152 loc) · 5.56 KB

appendix_bootstrap.asciidoc

File metadata and controls

206 lines (152 loc) · 5.56 KB

Bootstrap (aka Configuration Root)

Note
placeholder chapter, under construction

Congratulations on reading an appendix! Not everyone does. And yet we hide so much good stuff in here…​

OK at the end of [chapter_12_dependency_injection] we’d left a slightly ugly thing — there’s a circular dependency between flask_app.py and redis_pubsub.py. Also we had some duplication of boilerplate setup/init code in those two entrypoints, which felt a bit rough.

Explicitly defining a single "entrypoint" or bootstrap script or "configuration root" in OO parlance, is a pattern that can help us to keep things tidy. Let’s take a look.

Defaults and Config

Where do we declare our defaults? config.py is one place, but we also do some in, eg, unit_of_work.py, which declares the "default" database session manager. maybe that’s not too bad…​

Example 1. Default config declared next to uow (src/allocation/unit_of_work.py)
DEFAULT_SESSION_FACTORY = sessionmaker(bind=create_engine(
    config.get_postgres_uri(),
    isolation_level="SERIALIZABLE",
))

There is some other config spread around though, like what our "normal" dependencies are, and we haven’t even spoken about cross-cutting concerns like logging.

Other Setup Code: Initialisation

Defaults are maybe feeling a bit messy, but so are some other aspects of the initial setup or "bootsrapping" of our application; orm.start_mappers() for example, or uow.set_bus(). We call them in various places in our tests, and at least twice in our "real" application…​

Example 2. Flask calls start_mappers (src/allocation/flask_app.py)
app = Flask(__name__)
orm.start_mappers()
uow = unit_of_work.SqlAlchemyUnitOfWork()
bus = messagebus.MessageBus(
    uow=uow,
    ...
)
uow.set_bus(bus)



@app.route("/add_batch", methods=['POST'])
def add_batch():

Let’s bring all this stuff together into a single "bootstrap script" and see if we end up in a better position.

Bootstrap Script

Here’s what a bootstrap script could look like:

Example 3. A bootstrap function (src/allocation/bootstrap.py)
def bootstrap(
        start_orm=orm.start_mappers,
        session_factory=DEFAULT_SESSION_FACTORY,
        notifications=None,
        publish=redis_pubsub.publish,
):
    start_orm()
    uow = unit_of_work.SqlAlchemyUnitOfWork(session_factory=session_factory)
    if notifications is None:
        notifications = EmailNotifications(smtp_host=EMAIL_HOST, port=EMAIL_PORT)
    bus = messagebus.MessageBus(uow=uow, notifications=notifications, publish=publish)
    uow.set_bus(bus)
    return bus
  • it declares default dependencies but allows you to override them

  • it does the "init" stuff that we need to get our app going in one place

  • it gives us back the core of our app, the messagebus

Using Bootstrap in our Entrypoints

In our application’s entrypoints, we just call bootstrap.bootstrap() to get a messagebus, rather than configuring a UoW and the rest of it.

Example 4. Flask calls bootstrap (src/allocation/flask_app.py)
app = Flask(__name__)
bus = bootstrap.bootstrap()


@app.route("/add_batch", methods=['POST'])
def add_batch():
    ...
    bus.handle(cmd)
    return 'OK', 201

And in tests, we can use our bootstrap.bootstrap() with overridden defaults to get a custom messagebus:

Example 5. Overriding bootstrap defaults (tests/integration/test_views.py)
@pytest.fixture
def sqlite_bus(sqlite_session_factory):
    bus = bootstrap.bootstrap(
        start_orm=lambda: None,
        session_factory=sqlite_session_factory,
        notifications=mock.Mock(),
        publish=mock.Mock(),
    )
    yield bus
    clear_mappers()


def test_allocations_view(sqlite_bus):
    sqlite_bus.handle(commands.CreateBatch('b1', 'sku1', 50, None))
    sqlite_bus.handle(commands.CreateBatch('b2', 'sku2', 50, date.today()))
    sqlite_bus.handle(commands.Allocate('o1', 'sku1', 20))
    sqlite_bus.handle(commands.Allocate('o1', 'sku2', 20))

    assert views.allocations('o1', sqlite_bus.uow) == [
        {'sku': 'sku1', 'batchref': 'b1'},
        {'sku': 'sku2', 'batchref': 'b2'},
    ]

TODO: bootstrapper as class instead?

Dependency Diagrams

In chapter 9 (Dependency graph for chapter 9 (it’s a mess)), it’s a real mess:

chapter 09 dependency graph
Figure 1. Dependency graph for chapter 9 (it’s a mess)

By chapter 10 (Dependency graph for chapter 10 (it’s better)), when we introduce DI, things are much better:

chapter 10 dependency graph
Figure 2. Dependency graph for chapter 10 (it’s better)

Does the bootstrap script help? As Dependency graph with bootstrap script shows, the answer is: "kinda."

appendix bootstrap dependency graph 1
Figure 3. Dependency graph with bootstrap script

Well kinda-not actually. That Redis circular dependency is still there and looking ugly.

One fix is to split the "pub" from the "sub", as in Dependency graph with bootstrap script and no circular deps:

appendix bootstrap dependency graph 2
Figure 4. Dependency graph with bootstrap script and no circular deps

Now we have what our esteemed tech reviewer David Seddon would call a "rocky road architecture": all the dependencies flow in one direction.

TODO: alternative fix by making an abstract redis thingie?