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

Add a minimal Futures executor #65875

Closed
wants to merge 3 commits into from

Conversation

Matthias247
Copy link
Contributor

As disussed on Zulip within wg-async-foundations:

This change adds std::thread::block_on_future, which represents a minimal Futures executor.
It is modelled after futures-rs futures::executor::block_on, which blocks the current thread
until the Future had been driven to completion.

The implementation is a bit more efficient than the futures-rs one, since it doesn't require an
additional allocation and some refcount manipulations. It also does not require TLS, since it
makes use already existing TLS information about the current Threads state in the standard
library.

Feature-wise the main difference to the futures-rs implementation is that lacks the reentrancy
detection that is implemented in the futures-rs version. If someone calls block_on from
within an async fn there which already runs inside a block_on executor it will panic. This one
will not, and just spawn another executor. Depending on the code that runs in those executors
this can either work out fine, or lead to a deadlock issue, since the wrong executor handled the
remote wake() signal. However this isn't deemed as an issue, since wrong usage of APIs can
already lead to deadlocks in other places. Added the deadlock detection back in would require
another TLS variable.

Naming-wise I called this now block_on_future, but it's totally open for discussion. Just calling
it block_on gives a bit too less information on what it's about. I first had blcok_on_task - but
in the end most users won't know what a task is, and instead only observe it being passed a
Future as parameter. Something along [a]wait_future() might also work, but then maybe it's
mistaken with the await mechanism, which does something different.

@rust-highfive
Copy link
Collaborator

r? @cramertj

(rust_highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Oct 27, 2019
@rust-highfive
Copy link
Collaborator

The job x86_64-gnu-llvm-6.0 of your PR failed (pretty log, raw log). Through arcane magic we have determined that the following fragments from the build log may contain information about the problem.

Click to expand the log.
2019-10-27T18:50:06.1542157Z ##[command]git remote add origin https://github.com/rust-lang/rust
2019-10-27T18:50:06.1758349Z ##[command]git config gc.auto 0
2019-10-27T18:50:06.1840653Z ##[command]git config --get-all http.https://github.com/rust-lang/rust.extraheader
2019-10-27T18:50:06.1908059Z ##[command]git config --get-all http.proxy
2019-10-27T18:50:06.2067084Z ##[command]git -c http.extraheader="AUTHORIZATION: basic ***" fetch --force --tags --prune --progress --no-recurse-submodules --depth=2 origin +refs/heads/*:refs/remotes/origin/* +refs/pull/65875/merge:refs/remotes/pull/65875/merge
---
2019-10-27T18:56:28.3592314Z    Compiling serde_json v1.0.40
2019-10-27T18:56:30.1127307Z    Compiling tidy v0.1.0 (/checkout/src/tools/tidy)
2019-10-27T18:56:41.6571392Z     Finished release [optimized] target(s) in 1m 29s
2019-10-27T18:56:41.6650497Z tidy check
2019-10-27T18:56:42.4610661Z tidy error: /checkout/src/libstd/tests/block_on_future.rs: missing trailing newline
2019-10-27T18:56:42.4833005Z tidy error: /checkout/src/libstd/thread/block_on_future.rs: missing trailing newline
2019-10-27T18:56:42.4845324Z tidy error: /checkout/src/libstd/thread/mod.rs: missing trailing newline
2019-10-27T18:56:44.1190002Z some tidy checks failed
2019-10-27T18:56:44.1190872Z Found 484 error codes
2019-10-27T18:56:44.1191292Z Found 0 error codes with no tests
2019-10-27T18:56:44.1191383Z Done!
2019-10-27T18:56:44.1191383Z Done!
2019-10-27T18:56:44.1194976Z 
2019-10-27T18:56:44.1195076Z 
2019-10-27T18:56:44.1196640Z command did not execute successfully: "/checkout/obj/build/x86_64-unknown-linux-gnu/stage0-tools-bin/tidy" "/checkout/src" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage0/bin/cargo" "--no-vendor"
2019-10-27T18:56:44.1197278Z 
2019-10-27T18:56:44.1197367Z 
2019-10-27T18:56:44.1207448Z failed to run: /checkout/obj/build/bootstrap/debug/bootstrap test src/tools/tidy
2019-10-27T18:56:44.1207561Z Build completed unsuccessfully in 0:01:33
2019-10-27T18:56:44.1207561Z Build completed unsuccessfully in 0:01:33
2019-10-27T18:56:44.1265119Z == clock drift check ==
2019-10-27T18:56:44.1277148Z   local time: Sun Oct 27 18:56:44 UTC 2019
2019-10-27T18:56:44.2757054Z   network time: Sun, 27 Oct 2019 18:56:44 GMT
2019-10-27T18:56:44.2759500Z == end clock drift check ==
2019-10-27T18:56:45.6228345Z 
2019-10-27T18:56:45.6352677Z ##[error]Bash exited with code '1'.
2019-10-27T18:56:45.6394795Z ##[section]Starting: Checkout
2019-10-27T18:56:45.6397118Z ==============================================================================
2019-10-27T18:56:45.6397184Z Task         : Get sources
2019-10-27T18:56:45.6397422Z Description  : Get sources from a repository. Supports Git, TfsVC, and SVN repositories.

I'm a bot! I can only do what humans tell me to, so if this was not helpful or you have suggestions for improvements, please ping or otherwise contact @TimNN. (Feature Requests)

src/libstd/thread/block_on_future.rs Outdated Show resolved Hide resolved
src/libstd/thread/block_on_future.rs Outdated Show resolved Hide resolved
src/libstd/thread/block_on_future.rs Outdated Show resolved Hide resolved
src/libstd/thread/block_on_future.rs Outdated Show resolved Hide resolved
This change adds `std::thread::block_on_future`, which represents a minimal
Futures executor. It is modelled after futures-rs
`futures::executor::block_on`, which blocks the current thread until the
Future had been driven to completion.
// we would need to wrap the complete `Thread` object in another `Arc` by
// adopt the following line to:
// `let arc_thread = Arc::new(current());`
let arc_thread_inner = current().inner;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both &Context and &Waker are Send, so they might be send to another thread
(however unlikely it is to happen in practice). Thus it isn't a valid
optimization to delay obtaining the current thread.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's a bummer. I somehow missed that Waker is Sync - didn't remember we made it that way. Guess I will remove the optimization again.

LocalWaker would also have been nice for this use-case, but that's also gone.

@Matthias247
Copy link
Contributor Author

I pushed a change which removes the optimizations, since as pointed out by @tmiasko they were not allowed due to Waker being required to be Sync.

As it was pointed out in the review, the initial `Waker` for `block_on_future`
was not holding up the `Sync` guarantees. If the `Waker` reference had been
passed to another thread and cloned there, the cloned threadsafe `Waker` would
have captured the wrong `Thread` handle.

This change removes the optimization. A threadsafe `Waker` is now immediately
created.
@rust-highfive
Copy link
Collaborator

The job x86_64-gnu-llvm-6.0 of your PR failed (pretty log, raw log). Through arcane magic we have determined that the following fragments from the build log may contain information about the problem.

Click to expand the log.
2019-10-28T06:32:06.9846214Z ##[command]git remote add origin https://github.com/rust-lang/rust
2019-10-28T06:32:07.0233690Z ##[command]git config gc.auto 0
2019-10-28T06:32:07.0315408Z ##[command]git config --get-all http.https://github.com/rust-lang/rust.extraheader
2019-10-28T06:32:07.0375398Z ##[command]git config --get-all http.proxy
2019-10-28T06:32:07.0510527Z ##[command]git -c http.extraheader="AUTHORIZATION: basic ***" fetch --force --tags --prune --progress --no-recurse-submodules --depth=2 origin +refs/heads/*:refs/remotes/origin/* +refs/pull/65875/merge:refs/remotes/pull/65875/merge
---
2019-10-28T06:38:40.8498611Z    Compiling serde_json v1.0.40
2019-10-28T06:38:42.5306194Z    Compiling tidy v0.1.0 (/checkout/src/tools/tidy)
2019-10-28T06:38:53.1428741Z     Finished release [optimized] target(s) in 1m 23s
2019-10-28T06:38:53.1521584Z tidy check
2019-10-28T06:38:53.7420298Z tidy error: /checkout/src/libstd/tests/block_on_future.rs:255: trailing whitespace
2019-10-28T06:38:53.7594233Z tidy error: /checkout/src/libstd/thread/block_on_future.rs:116: trailing whitespace
2019-10-28T06:38:55.2248009Z Found 484 error codes
2019-10-28T06:38:55.2248789Z Found 0 error codes with no tests
2019-10-28T06:38:55.2249061Z Done!
2019-10-28T06:38:55.2252176Z some tidy checks failed
2019-10-28T06:38:55.2252176Z some tidy checks failed
2019-10-28T06:38:55.2254757Z 
2019-10-28T06:38:55.2261678Z 
2019-10-28T06:38:55.2262765Z command did not execute successfully: "/checkout/obj/build/x86_64-unknown-linux-gnu/stage0-tools-bin/tidy" "/checkout/src" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage0/bin/cargo" "--no-vendor"
2019-10-28T06:38:55.2262921Z 
2019-10-28T06:38:55.2262947Z 
2019-10-28T06:38:55.2262994Z failed to run: /checkout/obj/build/bootstrap/debug/bootstrap test src/tools/tidy
2019-10-28T06:38:55.2263077Z Build completed unsuccessfully in 0:01:26
2019-10-28T06:38:55.2263077Z Build completed unsuccessfully in 0:01:26
2019-10-28T06:38:55.2311952Z == clock drift check ==
2019-10-28T06:38:55.2325957Z   local time: Mon Oct 28 06:38:55 UTC 2019
2019-10-28T06:38:55.3044892Z   network time: Mon, 28 Oct 2019 06:38:55 GMT
2019-10-28T06:38:55.3051609Z == end clock drift check ==
2019-10-28T06:38:56.7259284Z 
2019-10-28T06:38:56.7396196Z ##[error]Bash exited with code '1'.
2019-10-28T06:38:56.7430328Z ##[section]Starting: Checkout
2019-10-28T06:38:56.7432524Z ==============================================================================
2019-10-28T06:38:56.7432586Z Task         : Get sources
2019-10-28T06:38:56.7432632Z Description  : Get sources from a repository. Supports Git, TfsVC, and SVN repositories.

I'm a bot! I can only do what humans tell me to, so if this was not helpful or you have suggestions for improvements, please ping or otherwise contact @TimNN. (Feature Requests)

@cramertj cramertj added the T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. label Oct 28, 2019
@cramertj
Copy link
Member

I don't feel super strongly about this, but I wouldn't expect this to be used in any real applications, and would expect this to be mostly useful for demo code. I guess that's a question for @rust-lang/libs about whether they want to include something like that in std.

@sfackler
Copy link
Member

block_on is useful when creating blocking APIs over async APIs, but that's a pretty niche use case.

@Matthias247
Copy link
Contributor Author

I think there are 2 use cases for this:

  1. having some simple executor that just allows for writing some async code without having to import a library. Unit tests and examples shared via play.rust-lang have been brought up as examples. Often those simple snippets don’t need IO, but will still have async signatures (in order to model the same API that a more comprehensive async application would have).
  2. Bridging sync and async code, as pointed out by @sfackler. I think it’s actually not as niche as thought of here. I have seen several work projects on other languages where the main application was running blocking code (eg a thread per request model), but async frameworks have been integrated in between. It allows to utilize helpful parts of the async ecosystem (eg applying atonement to any operation is trivial via select!) where necessary. And it can be used as a gradual migration path to a full async codebase.

For the latter there might however be some restrictions. Eg polling Futures that only work when polled from a certain executor (eg the Tokio one) will not work. It would work, if those frameworks separate the executor from reactors so that IO could also be performed from an outside executor like this one. This would in general be nice to have for improving interoperability in the async ecosystem. However since it might have performance implications we might need to live with the fact that not all async fns might work here. But still - some will work - and eg blocking on an async Channel to retrieve a notification from async subtasks can already be helpful and allow to build bridging APIs.

@withoutboats
Copy link
Contributor

I feel like it is too soon to do this. I have no strong opinion about whether it should be included in std, but that's because we haven't received enough feedback from users yet to make that decision confidently.

@Centril
Copy link
Contributor

Centril commented Oct 29, 2019

Even having this in a permanently unstable fashion in std would be useful for just trying things out wrt hacking on the compiler and whatnot.

@SimonSapin
Copy link
Contributor

I don’t have an opinion on this particular API, but I don’t think we should plan for anything intended to have external users to be permanently unstable. Instability should be a transition state until we either stabilize something, or deprecate and then remove it. (Though it’s ok if that transition takes a long time or is blocked on some other decision.)

@Matthias247
Copy link
Contributor Author

I feel like it is too soon to do this. I have no strong opinion about whether it should be included in std, but that's because we haven't received enough feedback from users yet to make that decision confidently.

This is a user-feedback :-) I had and have requirements to use async libraries from within plain old threads.

If you are interested in other data-points that provide it's helpful:

  • A search for runBlocking- which is the Kotlin equivalent for courtines - shows 23k usages on Github.
  • In the typical .NET codebase one probably sees as many Task.Wait() as await Task (either by accident or because async code is integrated into synchronous surroundings).

I also don't think the API is controversial and will get superseeded by something else more appealing, since it's super minimal. I could expect we might want a more advanced API later on, that e.g. allows spawning or that allows to run some methods in each eventloop integration (e.g. to drive timers). But I agree those might need to see more requirements and more design work. Or we might want to integrate a configurable cancellation hook for the future that gets polled - but that again could be exposed via another API.

May this is a bit far stretched, but I think this might also have a positive impact on interoperability between runtimes in the ecosystem: If a stdlib function allows to block on futures it might raise the priority for runtime authors to allowing polling and driving their Futures from outside executors (even though the resources used in those Futures might be managed by their runtime - incl IO reactors and timers).

@withoutboats
Copy link
Contributor

This is already available through the very fundamental futures library (not to mention the various runtimes having their own equivalent). Obviously people need a single threaded executor, but as you've already highlighted, there are some subtle design decisions here - mainly the issue around re-entrency (though I also think the exact API surface also matters). I think this should end up in std eventually, but I'd like to gather more feedback about these questions before putting it in.

Right now the behavior this PR implements seems to just be your preference (not clear who else has contributed to the decisions around naming and reentrancy you've made here)? That's the problem I'm highlighting: not enough people have used these APIs for us to make these choices at the level of confidence needed for std.

@JohnCSimon
Copy link
Member

Ping from triage:
what should happen with this PR? Should it be closed?
@cramertj @withoutboats @Matthias247

@withoutboats
Copy link
Contributor

I think a brief RFC about the proposed API would be a good next step here. Between the RFC text and the thread we could build a stronger consensus on the re-entrancy question, API name, etc.

@Luro02
Copy link
Contributor

Luro02 commented Nov 14, 2019

wouldn't std::task::block_on or std::future::block_on make more sense?

@withoutboats
Copy link
Contributor

I think @Matthias247's choice to put in std::thread is actually quite ingenious: it makes it much clearer that what this does is block the current thread, which is the really important thing to know about this function when deciding if its appropriate to use it.

@Matthias247
Copy link
Contributor Author

Right, this is in std::thread for a couple of reasons:

  • It is the current thread which gets blocked, not a task or a Future
  • task and future are actually all core/no-std modules. It seems great to keep them that way
  • There can be tasks and Futures without having any threads. At the end a Future is just a certain state of a computation. Therefore I think the whole ecosystem can be seen as as something more general than threads, and that functions on threads should depend on futures/tasks - and not the other way around.
  • it also helps a bit with efficiency to be able to have access to the thread modules internals

As mentioned earlier, I do not care too much about the name. I just chose block_on_future because it makes it more obvious what the thread is waiting for. Just having block_on might raise the question whether it's waiting for a timer, condvar, signal, etc...

I also don't have any strong opinion on the reentrancy question - apart from that I am pretty sure that it can not be supported without any issues (mostly deadlocks) anyway in a general fashion. That fact that I didn't add the reentrancy check is purely due to others having raised concerns on zulip around the use of thread-locals. But I am pretty much ok with either having fail-fast behavior (which requires the thread-local) or declaring reentrancy as undefined behavior (which is what this implementation is doing).

@withoutboats
Copy link
Contributor

or declaring reentrancy as undefined behavior (which is what this implementation is doing).

This statement surprised me but I think its a miscommunication (another way an RFC can help clarify things). We definitely can't declare reentrancy undefined behavior, because undefined behavior means it is the user's obligation to guarantee it never occurs, or all bets are off, including soundness and memory safety. Since this API is safe, it simply can't have undefined behavior.

But I don't see any undefined behavior in the code in the PR (maybe my oversight), so I think you probably meant implementation defined behavior - that is, we reserve the right to change the behavior in the future. But our experience has been that the stability guarantees we make for std make it very difficult to change the incidental behavior that users come to rely on. While I could imagine us making a change so that deadlocks become panics maybe (and even that I'm unsure of), it would be very difficult, essentially impossible for us to change the behavior of code that "works," at least in a stable API.

So we need to commit to an opinion about reentrancy before we can stabilize this API.

@realcr
Copy link

realcr commented Nov 21, 2019

Hey, I came here through @withoutboats post about Global Executors from TWiR, here: https://boats.gitlab.io/blog/post/global-executors/ , I just wanted to chip in my opinion.
Before anything else, thanks for everyone here working on async. I really appreciate it!

I am generally against having any kind of global or default executor inside std. I prefer to have some unified interface that will allow me to swap executors (and possibly reactors) seamlessly.
For sure I know less than the rest people in this discussion about the internals of how Futures work, but I have been using Futures in my Rust code since the first day they showed up in nightly, so at least I have a lot of experience as a Futures user.

My reasoning against Default/Global Executors

(1) A global executor can not be the best in everything for everyone, so most of the time users will use their own executor. Different people with different motives will have different ideas of how the global executor should be implemented. I use a special executor for tests (A deterministic single threaded executor, allowing time travel and breaking on deadlocks). I will definitely not use the global executor for my tests, unless it is also deterministic and allows me to break on deadlocks.

@Matthias247 wrote:

having some simple executor that just allows for writing some async code without having to import a library.

I think that this is something we can also say about random generators for example. I believe that we don't have a default random generator in std because everyone might have a different idea of what a good random generator is. Some people want fast random generators, and some people want cryptographically secure random, and even about what is really good cryptographic secure random people don't seem to agree. I think that this is a good thing. I have a very fine taste in random generation myself, and I like the fact that I get to pick my random generator myself.

(2) If the global executor becomes too convenient, people might actually use it. As a result, I expect that library writers will shove the executor arbitrarily inside their libraries. The problem with a code that contains a hidden executor is that it doesn't play nice with the rest of your async code. For example: I once wanted to have an http server, so I tried actix-web, and it turned out actix-web has its own executor. If I ended up using actix-web, my program would have contained two different executors!

This is something I believe could happen very often if a very convenient and "official" global executor exists. Looking at this from the opposite perspective, if there are no "official" global executors at all, I expect that library authors will have to be more agnostic about the executors being used.

I was working with async code in python for a very long time, first in Twisted and then in asyncio (previously Tulip). One of the things I disliked the most was the default loop ("loop" is how they call the Executor + Reactor in python). Many python libraries were using the default loop to spawn tasks, which made it very difficult to test any code that touched those libraries. I spent full nights trying to debug code, only to eventually find out that some task of some other library was spawned on a different loop than mine.

(3) Once we have an executor inside std, we might not be able to remove it because of backwards compatiblity.

Unified spawn interface instead of a Global Executor

I have been using futures-preview for a while, and now I ported my code to futures=0.3.1. This crate contains the Spawn trait. In my opinion this trait is a move in the correct direction. Many of my functions now take an impl Spawn as an argument. This allows me to easily replace the executor.

About the reactor: It is some kind of an invisible beast for me. I have never seen it, and all I know is that if I get things mixed up things end up not working silently, very much not what I'm used to have with Rust. See for example this issue: rust-lang/futures-rs#1285 , when I naively tried to spawn a future that uses a Tokio feature (timer) on a futures-preview's ThreadPool.

I will be happy to have an abstract reactor interface, allowing me to easily replace it.

@withoutboats wrote in the "Global Executors" article:

Ideally, many of these library authors would not need to spawn tasks at all. Indeed, I think probably most libraries which spawn tasks should be rewritten to do something else

Is it because spawning means allocation? I agree that library authors should strive to provide the user zero cost interfaces, but I think that sometimes you might want a library to be able to use your Executor.

Library example

Lets look at a simple example: an HTTP server library. The final function the library provide could be something like fn server() -> impl Future<...>, creating a future state machine. The user will then have the responsibility of spawning the future returned from the function.

However, the server itself might need to spawn tasks, for example, maybe for WebRTC, or some TLS periodic key replacement, or something else. Then in my opinion a reasonable solution would be to have a function with the following signature: fn server(spawner: impl Spawn) -> impl Future<...>. The user of the library would then provide his own spawner to the server. If I ever want to, I should be able to provide my test spawner to the server() function, or my super fast production spawner.

And if at this point we had a Global Executor, things could really go wrong. The library author might be tempted to take the easy route, and just use the Global Executor from std. In that case the function signature will still be fn server() -> impl Future<...>, but this time the server will internally use the Global Executor to spawn his tasks. (Generally this is what I dislike the most about global things: they sneak in behind your back and you can not spot them from the signature of the function. Those kind of things happen not very often with Rust, and I hoped to keep having it this way).

Now if I ever want to use my own executor I will not be able to provide it to server(), and my final running program will have more than one executor. In addition, writing any tests that include calls to server() in my code will become much more difficult.

@Matthias247
Copy link
Contributor Author

@realcr This issue is not about adding global executors to Rust as was mentioned in the blog post, nor around any abstraction which allows to configure an external global executor. The feedback therefore belongs in the internals.rust-lang.org discussion thread around this topic - not here. That discussion thread already captures a variety of feedback, which is partially similar to yours.

@realcr
Copy link

realcr commented Nov 22, 2019

@Matthias247 Hi, I was actually not sure where to post my opinion about this issue, the "Global Executors" post only had a link to this PR.

That said, I believe that what I wrote is relevant here. To quote what was written at the beginning of this PR:

This change adds std::thread::block_on_future, which represents a minimal Futures executor.

I think that having something like this inside std is a design decision that should be carefully considered, but except for commenting here relevant people do not have a way to voice their opinion about it.

Is there any corresponding RFC that describes the reasoning behind this PR? If there isn't, maybe we should formulate and RFC explaining the expected changes to std, and let people comment?

@withoutboats
Copy link
Contributor

withoutboats commented Nov 22, 2019

@realcr If you read the discussion on this PR, you'll see that it is blocked on someone creating such an RFC so that we can reach a consensus on the design questions in the block_on interface. However I agree with @Matthias247 that your comment is highly off topic for this PR or such an RFC and has to do with the other parts of my blog posts, which are (as @Matthias247 said) being discussed on the internals thread.

@Dylan-DPC-zz
Copy link

@withoutboats is this blocked on anything? Or just waiting to be reviewed?

@withoutboats
Copy link
Contributor

@Dylan-DPC In my opinion its a big enough API addition that it's blocked on an RFC.

@Dylan-DPC-zz
Copy link

Thanks. Updating it

@Dylan-DPC-zz Dylan-DPC-zz added S-blocked Status: Marked as blocked ❌ on something else such as an RFC or other implementation work. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Nov 24, 2019
@dtolnay
Copy link
Member

dtolnay commented Jan 10, 2020

I am excited to see an RFC for this too. I'll close this PR because I expect the RFC will incorporate the discussion here and we can follow up in a different PR after it plays out.

@dtolnay dtolnay closed this Jan 10, 2020
@Nemo157
Copy link
Member

Nemo157 commented Jan 16, 2020

Note for an eventual RFC/new PR: I believe this implementation has the same lost-wakeup issue with user-code calling Thread::{park, unpark} as futures::executor::block_on did before rust-lang/futures-rs#2010.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-blocked Status: Marked as blocked ❌ on something else such as an RFC or other implementation work. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet