Skip to content
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

Sharing db connection to nested nested app #2689

Closed
fduxiao opened this issue Jan 25, 2018 · 15 comments
Closed

Sharing db connection to nested nested app #2689

fduxiao opened this issue Jan 25, 2018 · 15 comments

Comments

@fduxiao
Copy link

fduxiao commented Jan 25, 2018

Long story short

I've read your answer in #2413 (raised in #2412 ).
The solution (subapp['db'] = mainapp['db']) does not work when it comes to nested subapps.
For example, if I have to build an app A, which have some subapp B and I need to pass the db connection from A to B, which can be done in that behavior. But, when I need to nest app A as a subapp to C, another app, and how should I pass db from C to A and to B. Clearly, the sharing between A and B exists before that between C and A, when A does not know the existence of db (which is even set inside startup) and C should also know nothing about the inner structure of A. A more complicated mechanism should be provided.

Expected behaviour

A more complicated solution.

Actual behaviour

Too simple to solve the problem.

Steps to reproduce

from aiohttp import web

app_b = web.Application()

app_a = web.Application()
app_b['db'] = app_a['db']  # error
app_a.add_subapp('/b', app_b)

app_c = web.Application()
app_c['db'] = 'db'  # actually inside a startup event
app_c.add_subapp('/a', app_a)  # how should I pass app_c['db']

web.run_app(app_c)

My solution

I'm trying to solve the problem by making the db become a promise (or a monad, which makes more sense about the side effect of the main app on the sub app ?).

from aiohttp import web


class MyApplication(web.Application):

    get_db = None

    def set_db(self, db):
        self.get_db = lambda: db

    def add_subapp(self, prefix, subapp):
        # The db should be a `promise` (like a monad).
        # When the `db` property is called,
        # the promise is then calculated.
        # So, when the db is set, a promise is set,
        # and when it is passed to the subapp, the promise is passed

        async def set_db(app):
            app.get_db = lambda: self.get_db()
        subapp.on_startup.append(set_db)
        #  subapp.get_db = lambda: self.get_db()
        return super().add_subapp(prefix, subapp)

While flask provides a g so I can share the db between different apps (with in the same request), will you consider adding a same mechanism (or a monadic implementation if your consider about the implicit side effect introduced by g), since not only the db is shared(more data should be shared?).

@asvetlov
Copy link
Member

We can organize a kind of chained maps, like https://docs.python.org/3/library/collections.html#collections.ChainMap
The chained mapping should be an explicit request's attribute most likely.

It breaks subapp isolation by definition but maybe we can live with it.
Need more thoughts and investigations.

@fduxiao
Copy link
Author

fduxiao commented Jan 26, 2018

May I suggest a concept called ‘runtime’. The instance is building a computation graph when handlers and start up progress are registered, which is used to describe when it should be run(triggered by signal). When the app is running, the triggered coroutine is called ‘runtime’. So the db is set during the runtime of startup. While now aiohttp will first triggered the startup runtime of the subapps, which leads to the absence of the db, we can add a node (of the graph) before the node of the startup of the sub-app, so we can ensure the existence of db at runtime. Thus in this manner, the outside app will first register a node to init db and the db config of the intermediate app is then triggered(which is set by the top app) and then the intermediate app triggers the db config of the inner apps(which means a new way to organize startup process should be introduced and the graph is built just as how those apps are nested like middlewares, or what if I put the add sub app process inside start up process? ). You can currently use a chain map instead of a list to organize startups and only the parent of a sub app will aware of it(parents know children but not vise versa). In addition, other frameworks like Django or Rails use class to describe their webapp(, which is already a graph by instinct). The instantiation of a class is actually an invocation of the runtime.

@asvetlov
Copy link
Member

The proposed graph-based solution is

  1. Powerful but too complex for average Python developer
  2. Can be built on top of aiohttp in separate library.

@fduxiao
Copy link
Author

fduxiao commented Jan 26, 2018

Or, is there any way to insert a startup before the startup of the sub app?

@asvetlov
Copy link
Member

I don't follow.
If you call app.add_subapp(pre, sub) you can do everything before the call or after it.

@fduxiao
Copy link
Author

fduxiao commented Jan 28, 2018

Yes, I can do everything before the calling, but the caller(intermediate app) may not be equipped with a db, but I now think out of a simpler solution. Just put the setup of a nested app inside the startup of the caller(based on your idea, or maybe I should read more docs :( ).
In the original code I put those process inside the nested apps, so came the problem.
Here's my example, and they work well

from aiohttp import web


# the inner app
app_a = web.Application()


async def index_of_a(request):
    return web.Response(text=request.app['db'])


app_a.router.add_get('/', index_of_a)


# the setup process of app a should be controlled inside 
# the startup of app b
async def setup_db_of_inner_app(app):
    app_a['db'] = app['db']


app_b = web.Application()
# a as the nested app of b
app_b.on_startup.append(setup_db_of_inner_app)
app_b.add_subapp('/a', app_a)

# the outer app
app_c = web.Application()


# take care of the setup of app b
async def db_of_app_a_and_setup_db_of_intermediate_app(app):
    # first setup the outer app
    app['db'] = 'the db'
    # then the intermediate, namely the `direct child` of outer app
    app_b['db'] = app['db']


app_c.on_startup.append(db_of_app_a_and_setup_db_of_intermediate_app)
app_c.add_subapp('/b', app_b)
web.run_app(app_c)

That's what I mean by insert a startup before the startup of the sub app.
Maybe you can put this lines in the faq so others won't suffer like me:) .
(A shorter version)

def plugin_app(app, prefix, nested):
    async def set_db(a):
        nested['db'] = a['db']
    app.on_startup.append(set_db)
    app.add_subapp(prefix, nested)

@fduxiao
Copy link
Author

fduxiao commented Jan 31, 2018

Actually I've found something more interesting. In the previous comment I made the mistake again but I didn't think it further, but now I think it's to be taken into consideration.
Consider the following:

from aiohttp import web


def plugin_app(app, prefix, nested):
    async def set_db(a):
        print(a.name)
        nested['db'] = a['db']

    app.on_startup.append(set_db)
    app.add_subapp(prefix, nested)


# the inner app
app_a = web.Application()
app_a.name = 'a'


async def index_of_a(request):
    return web.Response(text=request.app['db'])


app_a.router.add_get('/', index_of_a)


app_b = web.Application()
app_b.name = 'b'
app_c = web.Application()
app_c.name = 'c'
app_d = web.Application()
app_d.name = 'd'


plugin_app(app_b, '/a', app_a)
plugin_app(app_c, '/b', app_b)
plugin_app(app_d, '/c', app_c)

app_d['db'] = 'db'
web.run_app(app_d)

and

from aiohttp import web


def plugin_app(app, prefix, nested):
    async def set_db(a):
        print(a.name)
        a['db'] = app['db']

    nested.on_startup.append(set_db)
    app.add_subapp(prefix, nested)


# the inner app
app_a = web.Application()
app_a.name = 'a'


async def index_of_a(request):
    return web.Response(text=request.app['db'])


app_a.router.add_get('/', index_of_a)


app_b = web.Application()
app_b.name = 'b'
app_c = web.Application()
app_c.name = 'c'
app_d = web.Application()
app_d.name = 'd'


plugin_app(app_b, '/a', app_a)
plugin_app(app_c, '/b', app_b)
plugin_app(app_d, '/c', app_c)

app_d['db'] = 'db'
web.run_app(app_d)

The only difference is how I initialize the two apps.
The former one gives that

$ python3 run.py
d
c
b

but the later yields

$ python3 run2.py                                                     [6:01:50]
a
Traceback (most recent call last):
  File "run2.py", line 38, in <module>
    web.run_app(app_d)
  File "/usr/local/lib/python3.6/site-packages/aiohttp/web.py", line 422, in run_app
    loop.run_until_complete(app.startup())
  File "/usr/local/Cellar/python3/3.6.4_2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py", line 467, in run_until_complete
    return future.result()
  File "/usr/local/lib/python3.6/site-packages/aiohttp/web.py", line 259, in startup
    yield from self.on_startup.send(self)
  File "/usr/local/lib/python3.6/site-packages/aiohttp/signals.py", line 51, in send
    yield from self._send(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/aiohttp/signals.py", line 17, in _send
    yield from res
  File "/usr/local/lib/python3.6/site-packages/aiohttp/web.py", line 173, in handler
    yield from subsig.send(subapp)
  File "/usr/local/lib/python3.6/site-packages/aiohttp/signals.py", line 51, in send
    yield from self._send(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/aiohttp/signals.py", line 17, in _send
    yield from res
  File "/usr/local/lib/python3.6/site-packages/aiohttp/web.py", line 173, in handler
    yield from subsig.send(subapp)
  File "/usr/local/lib/python3.6/site-packages/aiohttp/signals.py", line 51, in send
    yield from self._send(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/aiohttp/signals.py", line 17, in _send
    yield from res
  File "/usr/local/lib/python3.6/site-packages/aiohttp/web.py", line 173, in handler
    yield from subsig.send(subapp)
  File "/usr/local/lib/python3.6/site-packages/aiohttp/signals.py", line 51, in send
    yield from self._send(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/aiohttp/signals.py", line 17, in _send
    yield from res
  File "run2.py", line 7, in set_db
    a['db'] = app['db']
  File "/usr/local/lib/python3.6/site-packages/aiohttp/web.py", line 94, in __getitem__
    return self._state[key]
KeyError: 'db'

I wonder how the order is controlled, by which I mean a kind of graph. Maybe I will still stick to the graph based solution. @asvetlov

@asvetlov
Copy link
Member

asvetlov commented Feb 1, 2018

Both snippets are identical and both generate KeyError exception

@fduxiao
Copy link
Author

fduxiao commented Feb 1, 2018

Updated.emoji

@asvetlov
Copy link
Member

asvetlov commented Feb 1, 2018

Just push 'db' into every subapplication explicitly, that's it.
Why do you want to take it from parent?

@fduxiao
Copy link
Author

fduxiao commented Feb 1, 2018

What if I write an app to be shared as a nested app among several projects and it has subapps and these subapps should have a db? The user of the app should also know nothing about the inner structure of the app(but the parent will tell the user it needs a db) and the db is initialized in the startup coroutines, which will not exist if I push it into every subapplication explicitly. That's why I have to pass it from the parent.

@asvetlov
Copy link
Member

asvetlov commented Feb 1, 2018

Is it an abstract question, do you have a real example?

Django has only one level of application nesting, Flask's blueprints cannot nested too.
Maybe you need 32 levels of nesting but you could pass DB everywhere with current aiohttp features.

@fduxiao
Copy link
Author

fduxiao commented Feb 2, 2018

Actually I write a restful route table first. Then I have user, blog and comment to use it. user has blog as its nested app and other utils and blog has comment as its nested app and other utils. So I just nested them and blog is built before user. That's why I need it. And current document makes me suffer.
Also I originally wrote like that

from aiohttp import web


def plugin_app(app, prefix, nested):
    async def set_db(a):
        print(a.name)
        a['db'] = app['db']

    nested.on_startup.append(set_db)
    app.add_subapp(prefix, nested)


# the inner app
app_a = web.Application()
app_a.name = 'a'


async def index_of_a(request):
    return web.Response(text=request.app['db'])


app_a.router.add_get('/', index_of_a)


app_b = web.Application()
app_b.name = 'b'


async def set_db(app):
    print(app.name)
    app['db'] = 'db'


app_b.on_startup.append(set_db)

plugin_app(app_b, '/a', app_a)

web.run_app(app_b)

it works but fails if I put a further nested app. Then I leave the issue.

@asvetlov
Copy link
Member

Fixed by #2949

@lock
Copy link

lock bot commented Oct 28, 2019

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a [new issue] for related bugs.
If you feel like there's important points made in this discussion, please include those exceprts into that [new issue].
[new issue]: https://github.com/aio-libs/aiohttp/issues/new

@lock lock bot added the outdated label Oct 28, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Oct 28, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants