Conversation
tricycle/_tests/test_tree_var.py
Outdated
| assert tv2.get() == 30 | ||
| assert tv2.get() == 10 | ||
|
|
||
| def mix_async_and_sync(): |
There was a problem hiding this comment.
This is my interpretation of its expected behavior when we mix sync and async context, let me know if any of it is not in line with what you have in mind!
oremanj
left a comment
There was a problem hiding this comment.
I appreciate the effort, but doing this correctly is going to be substantially more complicated than what you've posted so far.
Your approach is implicitly assuming that there is only one synchronous context in the whole program. That's not true though. You get one per thread to start with. You can create more -- contextvars.Context has a public constructor.
If we're doing something reasonable when there is no Trio current task, we need to distinguish two levels of "async context":
- If
trio.current_time()fails, there's no active Trio run at all. This is the "synchronous" case. We should set the TreeVarState in the current context using_cvar.set(). - If
trio.current_task()fails buttrio.current_time()doesn't, there is an active run but no active task. This state is observable primarily in instruments, end-of-run async generator finalizers, and someabort_fncallbacks. I think we should treat TreeVar modifications in this state as if they occurred in thetrio.lowlevel.current_root_task(). They will not propagate to any other task, because the nurseries underneath the root task have already been opened by the time any user-provided code runs.
When you access a TreeVar inside a Trio run whose value was set outside, you should get the value that was set in the context where trio.run() was called. That's available using trio.lowlevel.current_root_task().context. (This is not actually the enclosing context, it's a copy, but Trio doesn't make any changes to the copy, so it's a good reflection of what things looked like when trio.run() was called.)
We should also consider inheritance across threads. If you start a new thread using trio.to_thread.run_sync, it gets a copy of your contextvars context. So if you think you're in synchronous context, but your fetch returns a _TreeVarState that comes from a Trio task, your inherited value should come from the task that started the thread (which is not necessarily the same as the task that your TreeVarState comes from). You can determine the TreeVar value in that task using trio.from_thread.run_sync(self.get) and treat that as your inherited value.
tricycle/_tree_var.py
Outdated
| self._default = default | ||
| # Dummy trio task that that simulates the 'current task' | ||
| # whenever we're running in a synchronous context | ||
| self._sync_context_task = trio.lowlevel.Task._create( |
There was a problem hiding this comment.
I'm not really OK with this use of private APIs to create a trio task that trio doesn't know about. You'll need to figure out a different approach. I also see some red flags here like:
- why does every individual TreeVar need its own separate sync context? In reality isn't there only usually one context per thread in sync code?
- why are we capturing
copy_context()when the TreeVar is created? That might be different than the context of interest - for example, the TreeVar is probably a global and was created on the main thread, but it should be possible to start a new thread where you give it a value and then call trio.run() -- and if you do this multiple times, the different threads shouldn't conflict with each other.
There was a problem hiding this comment.
Makes sense, I created a separate dummy task class instead so we don't create trio tasks that trio doesn't know about.
The previous implementation is not correct because of the assumptions I made. I've addressed these 2 as well.
|
Thank you very much for the feedback! Apologies, I definitely was only addressing a single-threaded program when implementing the previous version. General approach for the current version: 1 'sync context task' per thread (applies to both trio thread and kernel thread). Store this task in I've also implemented 2 levels of async context.
This is the behavior in the previous and current version. I get the value from the sync context task for that particular thread. Let me know if I'm still missing anything! |
This change makes
TreeVarworks in sync code, and it behaves similarly to a normal contextvar in sync contextFixes #30