-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Discussion: Make ThreadContextElement or its analog available on all platforms #3326
Comments
Happy to provide additional context on the request if it helps. |
A bit of background around Snapshots and Compose
ConsistencyWhen a snapshot is taken the values of state objects are isolated from changes made in any other snapshot. They retain the values they had at the start of the snapshot regardless of any changes made outside the snapshot. This allows Compose to take a snapshot at the beginning of composition and produce frame that is consistent with a instantaneous snapshot of state at the beginning of composition. For example, if a composition produces a column of numbers and a sum of the column at the bottom, the sum is guaranteed to be the sum of the values displayed even if the numbers shown are actively changing by some background thread. This is because the changes are not visible to composition as composition takes a snapshot before it starts. ObservationAs changes are made to state objects, the Snapshot system will provide the equivalent of a flow of the objects changed (using a more primitive callback notification). Observers can register to be notified whenever a snapshot commits with changes. When a snapshot is created a callback can be supplied that notified whenever a state object is read. These two features combine to form a publish/subscribe system that allows composition to automatically subscribe to be notified when state objects it reads are modified, even indirectly. This is how Compose knows when and what to re-execute to update the composition. Global snapshotTransactions are typically hard to use as all modifications must be in a transaction and and committing that transaction might fail as it might contain changes that collide with changes that have already been committed by another transaction. Snapshots simplify this by allowing the user to trade consistency for availability by using the global snapshot. The global snapshot is the default snapshot a thread is in when the thread not explicitly in another snapshot. The global snapshot is guaranteed to commit and is committed before any new, top level snapshot is taken. This means that any changes made in the global snapshot are guaranteed to be accepted and visible to all new snapshots (existing snapshots are still isolated from the global snapshot). This allows most code to ignore snapshots, they just treat state objects as if they were normal mutable memory. If they need consistency, can then either use a synchronization mechanism of their choice (such as locks) or create a snapshot and make the changes in that, which might fail and require the work to be repeated. The users of snapshots always have a choice, make consistent changes that can fail or make potentially inconsistent changes that will always succeed. Snapshots and coroutinesCurrently snapshots are of limited use in coroutines as they rely on a thread local variable to preserve the isolation for a thread. As coroutines hop threads the thread local variable is not carried with the coroutine. The However, a context element is helpful but not sufficient if used with structured concurrency. For snapshots to work well with structured concurrency, as the coroutine splits and joins the snapshot should also split and join. If the coroutine is cancelled then the snapshot it is using should also be cancelled. Snapshots support nested snapshots. That is, in any snapshot a child snapshot can be taken and when it is applied (a.k.a. committed) it is applied to the parent snapshot. When used with structured concurrency, whenever a child context is created a corresponding child snapshot should be taken. When the child context joins the parent then the snapshot should be applied to the parent. If, when applying the child snapshot, it fails because of a conflicting change, the corresponding context should be cancelled with an exception. The context element helps bring coroutines on level footing with threads when using snapshots. This is the motivation for asking for the equivalent of of a thread context element. Allowing snapshot state to track the structure of concurrency allows them to be truly useful. This is the motivation for additional hooks into coroutines. |
SnapshotContextElementImpl uses ThreadContextElement which is available only for jvm targets, therefore it can't be used in common source sets. (There is no alternative for k/native and k/js yet. The discussion is here Kotlin/kotlinx.coroutines#3326) Also, updated SnapshotContextElementTests to use coroutines test API available for all platforms. Test: ./gradlew :compose:runtime:runtime:test Change-Id: I3e5e74b5f5a4a804bab835a3e7c4493e0e19fe36
Thanks for the detailed explanation!
I'm not sure this is what should happen if there's a merge conflict. What if we were going to resume the coroutine with an exception anyway? We'd have two exceptions to choose from in that case. The one with which we wanted to resume the coroutine anyway is probably expected and properly handled, whereas a merge conflict looks like a more severe issue. I agree with you that it should at least propagate throughout the coroutine hierarchy, but let's expand on this: perhaps we could use the uncaught exception handler for this instead? If merge conflicts are considered purely a programming mistake (so, not a normal mode of operation), crashing an app by default could be a proper solution. If someone does need explicit handling of such exceptions, it's possible to override such behavior by registering an uncaught exception handler. What do you think? |
@dkhalanskyjb Thank you for reply! Sorry for a long absence.
As an option, maybe overriding the Result.Failure can be restricted. If there was an exception before, we probably shouldn't even attempt to apply the snapshot.
I'm not sure that a merge conflict when applying snapshots ( Let's hear from Adam and Chuck on this. cc: @adamp @chuckjaz P.S. Back then, I made a simple prototype of ThreadContextElement for k/js and k/native #3325, but of course it doesn't solve all extra questions. |
Dispose (a.k.a abort) the snapshot. A snapshot does not need to be applied. If the coroutine is going to cancel with an exception then its snapshot should be disposed(). If the developer wants the different behavior they can, for example, commit the snapshot in a catch or finally and they can then decide how to handle conficts. The default behavior should be to discard the snapshot.
It depends on the context of how the snapshot is being used. For the states created in composition, they should only ever be modified during composition and, since is never multiple coroutines operating on the same composition, merge conflicts are hard to generate, and rely on give a reference to state created in composition to some outside observer, and are be considered fatal errors. However, if the state is part of the application model then snapshot failures would be more common depending on how the mutators are controlled (unique mutators of state cannot conflict). The default should be that conflicts should throw an exception (e.g. What I would like to be able to do is have a context element observe the splits and joins of the coroutine scopes for which it is an element. That is, if the coroutine splits a new element should be able to be created for the new context and then, when it later rejoins the parent, the element should be able to merged back to the parent element. It would also be nice to know when the resources used by the element can be discarded. If I had such an API, snapshots would be much more useful. Without it (and with thread elements) it is still useful but requires a lot of hand-holding. This same mechanism would be generally useful for any pooled resource (a pool of database connections, for example) as resource aquired by a coroutine context can be returned as a natural part of the structure of the coroutine. Snapshot are, if you squint hard enough, a pooled resource and would benefit from any support for pooled resources. |
Another use-case: On JVM, you could use ThreadContextElement, like Exposed does, but there is no Thread/WorkerContextElement on Kotlin Native to (re-) store a database connection in the current context based on the underlaying executing worker. |
Currently, ThreadContextElement interface is available only for jvm and it has 2 functions: 1st invoked before a coroutine is resumed, 2nd invoked after that coroutine is suspended.
These functions should never throw an exception:
The need to have it for all targets (jvm, k/js, k/native) appeared in Compose Runtime library. Currently it's used in JVM only, but something similar needed to provide correct implementations of new Compose API for k/js and k/native .
Use Case
Here is a test from compose to showcase an example of desired behaviour:
A coroutine can read/write a snapshot by accessing it via
Snapshot.current
(even if there is no SnapshotContextElement, Snapshot.current will return a snapshot from either a ThreadLocal or a global snapshot).SnapshotContextElement
lets us have independent snapshots in different coroutines within one thread, so a coroutine can have an associated Snapshot.ThreadContextElement
can be further helpful to performSnapshot.apply
(commit changes) inrestoreThreadContext
, when a coroutine suspends.Not existing feature: (asked by Adam Powell)
Let's say we want to commit a Snapshot when coroutine with SnapshotContextElement suspends: so we call
Snapshot.apply
fromThreadContextElement.restoreThreadContext
. The thing isapply
can returnSnapshotApplyResult.Failure
.In that case, it's desirable to resume a suspended coroutine right away with an exception. It would need to be an arbitrary exception and not a CancellationException, as it should eventually result in failing the job tree.
Question: Can we somehow have an access to a continuation of a suspended coroutine within a CoroutineContext.Element (right after it was suspended)?
Question to conclude the above:
restoreThreadContext
?The text was updated successfully, but these errors were encountered: