You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Is there a reason to do task.task_local = get_local() or {} instead of just task.task_local = {}? Because now if you spawn a task inside of another task, you get the same local context for both tasks ant this may cause very strange errors.
And yes as you mentioned, this caused side effect that even if the sub task is never awaited, it shares the same context:
asyncdefmain():
get_local()['val'] =123loop.create_task(sub())
asyncdefsub():
print(get_local().get('val')) # still 123
There are cases where not all sub calls are plain coroutines but tasks which need the same context (asyncio.wait_for for example), and it is a less hacky way to achieve so at task creation period. I've tried to pass the context to a sub task only when it is joined into current task, with a lot of hacks in Task._step(), which have problems with non-default Task implementations.
So before PEP-550, I suppose we'll need to make decision on whether to share context at task creation, or not to share context between tasks at all.
Also if possible, please share a bit more about the strange errors you met, which might be helpful for the decision.
I always thought of the create_task / ensure_future as a way to spawn a completely detached task (mostly because you can usually replace constructions like await loop.create_task(sub()) with simple await sub()). That's why such inheritance surprised me.
As for my case. In my app, I have a bunch of background processes, each of them creates a transaction and queries a database once per few seconds. Each process is a coroutine with an infinite loop inside it:
The init function called via loop.run_until_complete(init()).
As you can see, all background tasks share the same context. This sometimes causes 'wrong release order' and 'connection closed in the middle of operation' errors.
An easy solution would be to create a function which enables or disables inheritance:
awaitloop.create_task(inherit_context(sub()))
The implementation might look like this:
classinherit_context:
def__init__(self, coro=None):
self._coro=coroself._local=get_local()
asyncdef__await__(self):
# An example implementation. We should think whether we want to update# or to replace local context completely.get_local().update(self._local)
returnawaitself._coro
changed the title [-]Why inherit task locals?[/-][+]Spawned tasks shouldn't inherit task local storage from creator task if not joined[/+]on Oct 11, 2017
sub1 and sub2 create tasks inside gather and must share transaction by default
This sounds reasonable. I've also realised that cancellation signal would be sent to any subtask that was spawned in the task that's being cancelled and there's a shield function to protect subtasks from cancellation. So this seems to be ok to inherit locals by default.
@smagafurov That makes sense, thanks for the proposal. Feature added on task object, please see above commit. Should it also accept coroutines/awaitables and create non-inheriting tasks internally? That would require additional parameter "loop".
True, db.transaction(reuse=False) does exactly what "separate transaction" indicates.
separate_task stands in somewhere between the two - it is not about transactions, not about local storages. I'm not quite a native English speaker, to "separate task" is a bit confusing to me - what is actually separated, when we could still do await separate_task(task).
Though, I believe your attempt was on the right direction (much appreciated!), which inspired me to check how local storage is used and what this function really affects. The local storage is the main part behind the scene of db.acquire(reuse=True) or db.transaction(reuse=True), which is, to reuse current connection if there is one in the context. By resetting the local storage, we simply created a new context and thus prevented previous connection from being reused in the given coroutine. Therefore I propose replacing reset_local with forget_connection - meaning previous connection is simply forgot in the given task. Works both as transparent task wrapper, and inline action without parameter:
fromginoimportforget_connection, is_root_connectionasyncdefmain():
res=awaitforget_connection(sub()) # make sub forget current connection if anyloop.create_task(sub_inline())
asyncdefsub_inline():
forget_connection() # try to forget inherited connectionnew_conn=awaitdb.acquire() # then we get brand new connection, even with reuse=Trueprint(new_conn.is_root) # True
Meanwhile, I'll deprecate enable_task_local, disable_task_local and get_local on gino level, and add enable_connection_reuse and disable_connection_reuse in replace, while keeping methods as they are on gino.local level in case task local as a separate module might be useful. Also I'll expose LazyConnection._root is self as is_root as shown in above example. Please feel free to raise better ideas or it will be so.
Therefore I propose replacing reset_local with forget_connection
I disagree. Task local storage can be used for more than just storing a connection stack. For example, I store logging information, sentry context, a reference to the current request there. By replacing reset_local with forget_connection you'll force users to implement their own task local storages. They'll than have to use their own methods to forget theirs local storage and they'll have to be compatible with gino's implementation. There also be issues with double-patching task factory and so on.
User should not know about local storage inside at all.
Why shouldn't they? I mean, you have to understand the tool you are using. And this is critically important when talking about local storage.
An example: will transaction be inherited if we use loop.call_soon(f)? The answer is no, because f will be called outside of any task. On the other hand, when using loop.create_task(f()), transaction will be inherited.
We'll do a disservice to our users by hiding these implementation details.
When pep 550 released, we change our inner realization of separate_transaction to execution context usage
When pep-550 released, we indeed can hide our implementation, because pep-550 proposes a uniform way to manage local contexts and (ideally) every python programmer will know how it works and how to avoid problems described above.
I'm kinda with @AmatanHead on this. Please see commit 820990c - gino/local.py itself is still a self-contained module which is relatively independent to gino itself. And how GINO "renames" the locals API looks a bit embarrassing - it is only a renaming and does nothing else, that's why this commit is not included in PR #85. I think perhaps it's better to move local.py into a new project completely, and make it an optional dependency in GINO. I dislike hundreds of dependencies too (manually at@ NPM here), but maybe this one is worth it. After PEP-550 released, users could simply stop using local.py while everything is still the same because GINO should then internally switch to use PEP-550 with a higher priority. (Of course local.py should also adapt PEP-550 to its API so that it is also okay not to drop local.py immediately after PEP-550).
Thanks everyone for your precious input! It had been off topic a bit, so I simply merged #85 as it is, closed this issue and created #89. Please kindly let me know if you have further ideas about the renaming or creating new package there. Much appreciated!
Activity
AmatanHead commentedon Oct 10, 2017
I propose removing this logic (at least I don't see any reason to keep it).
fantix commentedon Oct 11, 2017
This was intentional, because I'd like to make context shared in such case:
And yes as you mentioned, this caused side effect that even if the sub task is never awaited, it shares the same context:
There are cases where not all sub calls are plain coroutines but tasks which need the same context (
asyncio.wait_forfor example), and it is a less hacky way to achieve so at task creation period. I've tried to pass the context to a sub task only when it is joined into current task, with a lot of hacks inTask._step(), which have problems with non-defaultTaskimplementations.So before PEP-550, I suppose we'll need to make decision on whether to share context at task creation, or not to share context between tasks at all.
Also if possible, please share a bit more about the strange errors you met, which might be helpful for the decision.
AmatanHead commentedon Oct 11, 2017
I always thought of the
create_task/ensure_futureas a way to spawn a completely detached task (mostly because you can usually replace constructions likeawait loop.create_task(sub())with simpleawait sub()). That's why such inheritance surprised me.As for my case. In my app, I have a bunch of background processes, each of them creates a transaction and queries a database once per few seconds. Each process is a coroutine with an infinite loop inside it:
All of those tasks start in the init procedure (init is async because I want to keep all initialisation, both sync and async, in the same place):
The init function called via
loop.run_until_complete(init()).As you can see, all background tasks share the same context. This sometimes causes 'wrong release order' and 'connection closed in the middle of operation' errors.
An easy solution would be to create a function which enables or disables inheritance:
The implementation might look like this:
fantix commentedon Oct 11, 2017
I see, it is a critical issue in your case then. Thanks for the suggestion, which inspired me to try something, one sec I'll test and update.
Fixed #84, don't passthrough task local to spawned tasks which are no…
[-]Why inherit task locals?[/-][+]Spawned tasks shouldn't inherit task local storage from creator task if not joined[/+]smagafurov commentedon Oct 11, 2017
Some proposal:
May be you can provide ability to explicitly disable inheritance
But implicitly (by default) we need to keep inheritance
Because we need this to work:
sub1 and sub2 create tasks inside gather and must share transaction by default
if we need separate tasks - we can do it explicitly
AmatanHead commentedon Oct 11, 2017
This sounds reasonable. I've also realised that cancellation signal would be sent to any subtask that was spawned in the task that's being cancelled and there's a
shieldfunction to protect subtasks from cancellation. So this seems to be ok to inherit locals by default.Refs #84, added ability to manually reset local storage from inheriti…
fantix commentedon Oct 11, 2017
@smagafurov That makes sense, thanks for the proposal. Feature added on task object, please see above commit. Should it also accept coroutines/awaitables and create non-inheriting tasks internally? That would require additional parameter "loop".
6 remaining items
smagafurov commentedon Oct 12, 2017
IMHO
separate_taskis good name for thisseparate_transactionmay assume that we start separate transaction for this task but we don'tfantix commentedon Oct 13, 2017
True,
db.transaction(reuse=False)does exactly what "separate transaction" indicates.separate_taskstands in somewhere between the two - it is not about transactions, not about local storages. I'm not quite a native English speaker, to "separate task" is a bit confusing to me - what is actually separated, when we could still doawait separate_task(task).Though, I believe your attempt was on the right direction (much appreciated!), which inspired me to check how local storage is used and what this function really affects. The local storage is the main part behind the scene of
db.acquire(reuse=True)ordb.transaction(reuse=True), which is, to reuse current connection if there is one in the context. By resetting the local storage, we simply created a new context and thus prevented previous connection from being reused in the given coroutine. Therefore I propose replacingreset_localwithforget_connection- meaning previous connection is simply forgot in the given task. Works both as transparent task wrapper, and inline action without parameter:Meanwhile, I'll deprecate
enable_task_local,disable_task_localandget_localonginolevel, and addenable_connection_reuseanddisable_connection_reusein replace, while keeping methods as they are ongino.locallevel in case task local as a separate module might be useful. Also I'll exposeLazyConnection._root is selfasis_rootas shown in above example. Please feel free to raise better ideas or it will be so.AmatanHead commentedon Oct 14, 2017
I disagree. Task local storage can be used for more than just storing a connection stack. For example, I store logging information, sentry context, a reference to the current request there. By replacing
reset_localwithforget_connectionyou'll force users to implement their own task local storages. They'll than have to use their own methods to forget theirs local storage and they'll have to be compatible with gino's implementation. There also be issues with double-patching task factory and so on.Why shouldn't they? I mean, you have to understand the tool you are using. And this is critically important when talking about local storage.
An example: will transaction be inherited if we use
loop.call_soon(f)? The answer is no, becausefwill be called outside of any task. On the other hand, when usingloop.create_task(f()), transaction will be inherited.We'll do a disservice to our users by hiding these implementation details.
When pep-550 released, we indeed can hide our implementation, because pep-550 proposes a uniform way to manage local contexts and (ideally) every python programmer will know how it works and how to avoid problems described above.
Fixed #84, added ability to reset local.
Refs #84, naming tweaks and added `LazyConnection.is_root`
Renamed refs #84
fantix commentedon Oct 15, 2017
I'm kinda with @AmatanHead on this. Please see commit 820990c -
gino/local.pyitself is still a self-contained module which is relatively independent toginoitself. And how GINO "renames" the locals API looks a bit embarrassing - it is only a renaming and does nothing else, that's why this commit is not included in PR #85. I think perhaps it's better to movelocal.pyinto a new project completely, and make it an optional dependency in GINO. I dislike hundreds of dependencies too (manually at@ NPM here), but maybe this one is worth it. After PEP-550 released, users could simply stop usinglocal.pywhile everything is still the same because GINO should then internally switch to use PEP-550 with a higher priority. (Of courselocal.pyshould also adapt PEP-550 to its API so that it is also okay not to droplocal.pyimmediately after PEP-550).Refs #84, naming tweaks and added `LazyConnection.is_root`
Fixed #84, added ability to reset local.
Merge pull request #85 from fantix/t84
fantix commentedon Oct 15, 2017
Thanks everyone for your precious input! It had been off topic a bit, so I simply merged #85 as it is, closed this issue and created #89. Please kindly let me know if you have further ideas about the renaming or creating new package there. Much appreciated!
Fixed python-gino#84, added ability to reset local.
Norfolks commentedon Oct 23, 2019
Just for people who came here with such error and want to make independent context for create_task. I took some time for me, before I got this.
fantix commentedon Oct 23, 2019
@Norfolks thanks for the info! See also here for Python < 3.7: #572 (comment)