If you are a hyperscaler, you are not using async/await. For a hyperscaler, the cost of managing their server infrastructure may be in the billions. Async/await is an abstraction. You’ll not want to use it. To really get the most out of your performance, you are better of modifying the kernel to your needs.
I don’t think these people are using async/await, and for good reasons.
I obviously can’t speak for all the hyperscalers, but a lot of folks at AWS sure are using async/await, increasingly alongside io_uring to great effect. There’s always money in performance, of course, but one of the great things about the Rust ecosystem is how easy it makes it to get to “really very good” performance.
As a concrete example, when we were building AWS Lambda’s container cache, we build a prototype with hyper and reqwest and tokio talking regular HTTP. We totally expected to need to replace it with something more custom, but found that even the prototype was saturating 50Gb/s NICs and hitting our latency and tail latency targets, so just left them as-is.
In reality, your OS scheduler does the exact same thing, and probably better.
I think the reality is that, for high-performance high-request-rate applications, a custom scheduler can do much better than Linux’s general purpose scheduler without a whole lot of effort (mostly because it just knows more about the workload and its goals).
You have to be (1) working at a large organization, (2) be working on a custom web server (3) that is highly I/O bound.
Yeah, for sure. But the async model made the code clearer and simpler then the equivalent threaded code would have been, when taking everything into account (especially the need to avoid metastability problems under overload, which generally precludes a naive thread-per-request implementation).
I do appreciate the tail latency angle. There are some (synthetic) benchmarks that show async/await being superior here. (Of course this depends on the async runtime too.) On the other hand, it seems to me a bit too niche requirement to justify async/await, assuming overload is not a very common situation. I am assuming async/await being a worse experience here. And reading from your comment you did not have that experience.
Services of all sizes need to protect against overload. My understanding is that async/await enables the server to handle as much load as the CPU(s) can handle without having to tune arbitrary numbers (e.g. thread pool size), and allowing the network stack to apply natural backpressure if the load increases beyond that point.
Edit to add: If it seems that designing a service to gracefully handle or prevent overload is a niche concern, perhaps that’s because we tend to throw more hardware at our services than they really ought to need. Or maybe you’ve been lucky enough that you haven’t yet made a mistake in your client software that caused an avoidable overload of your service. I’ve done that, working on a SaaS application for a tiny company.
When using tokio (and this goes for most async runtimes) it is actually not recommended to use async/await for CPU-bound workloads. The docs recommend using spawn_blocking which ends up in a thread pool with a fixed size: https://docs.rs/tokio/latest/tokio/#cpu-bound-tasks-and-blocking-code
When using tokio (and this goes for most async runtimes) it is actually not recommended to use async/await for CPU-bound workloads.
No, that’s about hogging up a bunch of CPU time without yielding. It doesn’t apply if you have tasks that use a lot of CPU in total but yield often, or simply have so many tasks that you saturate your CPU. I’m pretty sure the latter is what @mwcampbell was referring to with “enables the server to handle as much load as the CPU(s) can handle”.
Tail latency is extremely important and undervalued. This is why GC languages are unpopular in the limit, for example — managing tail latencies under memory pressure is very difficult.
edit: of all groups of lay people, I think gamers have come to understand this the best. Gamers are quite rightly obsessed with what they call “1% lows” and “0.1% lows”.
As an AWS user, I can say you can saturate S3 get objects calls with async await pretty easily as well, to the point where there’s a few github issues about it. https://github.com/awslabs/aws-sdk-rust/issues/1136 <– essentially you have to hold your concurrency to between 50-200 depending on where you’re situated wrt s3.
Like many (most?) posts that are labelled as being against async/await in Rust, this one seems to actually be against Tokio:
In the default tokio configuration, the async runtime will schedule tasks across many threads, to maximize performance. […] Even worse, you now need to choose between std::sync::Mutex and tokio::sync::Mutex. Picking the wrong one could adversely affect performance.
and
Standard Rust threads can be “scoped”. Tokio tasks do not support it. This one is also likely never going to be fixed, since there is a fundamental issue making it impossible.
and
In async Rust, [closing a temp file via RAII] is not possible since tokio::fs::remove_file must be called from an async function,
and
Did you know Tokio will sometimes put your tasks on the heap when it believes they are too big?
and
Anytime a library adopts async/await, all its consumers must adopt it too. Aysnc/await poisons across project boundaries.
Tokio! Tokio! And more Tokio!
Reading slightly between the lines, the author seems to have started out with the assumption that they need an N:M userspace thread runtime for good I/O performance (because of a background in C# ?), then they found Tokio (an N:M userspace thread library that uses async as one part of its implementation), and they’re having trouble getting Tokio to deliver on what the author expects of it.
Maybe that’s the author’s fault, maybe it’s Tokio’s fault; I haven’t looked at the author’s code and therefore can’t judge either way. But it seems clear that it’s not Rust‘s fault, because as the author notes the async/await model works great for (1) embedded environments and (2) multiplexing I/O operations on a single thread, which are exactly the use cases that Rust’s async/await is designed to solve.
Maybe the answer is for the Rust project to intentionally de-emphasize Tokio in its async/await documentation? Over and over it seems that every time I try to use Tokio in my own projects I stumble into weird and non-Rustic behavior (e.g. the implicit spilling to the heap mentioned in this post), and every time I see an experienced programmer struggling with async/await the problems all seem to revolve around Tokio in some capacity.
Maybe the answer is for the Rust project to intentionally de-emphasize Tokio in its async/await documentation?
I doubt that would meaningfully help.
From my perspective, it’s down to a “Nobody ever got fired for choosing IBM” attitude around Tokio, stemming from “I can trust that every dependency I might need supports Tokio. I don’t want to slam face-first into the hazard of some required dependency not supporting async-std or smol or what have you”.
I think we’re just going to have to await 😜 more things like “async fn in traits” (Rust 1.75) landing as building blocks for looser coupling between dependencies and runtimes.
(Also, Ugh. Lobste.rs apparently doesn’t have an onbeforeunload handler for un-submitted posts and I accidentally closed the tab containing the previous draft of this after getting too comfortable to compose it in a separate text editor and then paste it over.)
Reading slightly between the lines, the author seems to have started out with the assumption that they need an N:M userspace thread runtime for good I/O performance (because of a background in C# ?), then they found Tokio
No, it’s because the existing library ecosystem is centered around tokio. If you’re not writing things yourself, you’re probably going to be using tokio. It’s the unofficial official Rust runtime.
If you’re not writing things yourself, you’re probably going to be using tokio. It’s the unofficial official Rust runtime.
I’ve heard this a lot, but it just doesn’t seem to be true – Tokio is popular but by no means universal, and it’s silly to act as if it’s somehow more official than (for example) embassy or glommio.
Most Rust libraries are written for synchronous operation. It’s a small minority that use async at all, and even fewer of those hardcode the async runtime to Tokio. Not to mention that the use cases for which Rust has a clear advantage over Go/Java/C#/etc are things like embedded, WebAssembly, or dynamic libraries – none of which are supported by heavy N:M runtimes such as Tokio.
To me tokio is a bunch of decisions made for me. At first when I saw it I disagreed with most of them in some way or another. After I couldn’t avoid it, I realized in the end this isn’t a half bad way of providing parallelism and for instance the alternative to “spilling to the heap” is essentially crashing.
What I think is scary about tokio for new users, and I’m planning a little post on, is that if you start going deeply async you end up unbounded, possibly with an explosion in the number of tasks depending on how your code is written. You can hit external limits, etc. Controlling that can only be done (afaict) with a tuned Arc limit from the root passing down to the overspawned task. To me it’s a small price for how easy writing and maintaining it is.
Some of the issues are Tokio specific, some are not. Either way thinking about async/await on purely a language-level is not helpful. Everyone has to pick a runtime and due to lock-in a majority will end up with tokio. Whether the issue stems from Tokio or Rust’s language design ultimately does not matter to me as a programmer that wants my code to work.
But you frame the article as a general critique against async/await, saying htat most of what you say should apply even to other languages!
So how about the web servers that run the web right now? Interestingly enough, nginx is written in C, and does not use async/await. Same for Apache. Those two servers happen to be the most widely used web servers and together serve two thirds of all web traffic. Both of them do use non-blocking I/O, but they do not use async/await. They seem to do pretty well regardless. (Note that my beef is specifically with async/await, not with non-blocking I/O.)
[..]
If you are a hyperscaler, you are not using async/await. For a hyperscaler, the cost of managing their server infrastructure may be in the billions. Async/await is an abstraction. You’ll not want to use it.
Are you aware that one hyperscaler, Cloudflare, replaced Nginx with an in-house proxy written in Rust using Tokio, in part due to issues with tail latencies and uneven load balancing between cores?
Yes (link here for those interested: https://blog.cloudflare.com/how-we-built-pingora-the-proxy-that-connects-cloudflare-to-the-internet/). My reading is that the performance issues were due to nginx not reusing connections across cores, which could be solved without Tokio. Cloudflare could have opted to use mio directly for example. On the other hand I do understand their choice to use Tokio here because it probably helped them ship much faster. I would not be surprised if they eventually swap out Tokio for a custom runtime (or maybe even use mio directly) since there would probably be some extra performance to be gained.
Note that I edited my blog post a bit to reflect the fact that hyperscalers are using async/await.
My reading is that the performance issues were due to nginx not reusing connections across cores, which could be solved without Tokio.
That’s not entirely correct. They also had difficulty with nginx’s thread-per-core model which precludes work-stealing.
Cloudflare could have opted to use mio directly for example.
Why would they use mio directly and implement a work stealing scheduler atop of Mio? To me, this seems like Tokio, but with extra steps.
I would not be surprised if they eventually swap out Tokio for a custom runtime (or maybe even use mio directly) since there would probably be some extra performance to be gained.
I would be. A work-stealing scheduler is almost certainly what most people want/need, but it is especially optimal for load balancers/proxies.
Note that I edited my blog post a bit to reflect the fact that hyperscalers are using async/await.
I know you edited this, but the alternative to using async/await is pervasive, feral concurrency control where people constantly spin up new thread pools. async/await, in Rust, is substantially more efficient than the alternatives that people would otherwise gravitate to.
It’s extremely easy to find async code that doesn’t use Tokio. And in any case, this article claims to be a general criticism of the whole async/await paradigm, regardless of runtime or language.
The other solutions end up looking very similar to async, but without the syntax sugar to make the code linear.
Timeouts and cancellation are important for reliability, but unfortunately the sync APIs we have suck at that. They’re all forever-blocking non-cancellable by default. Timeouts are ad-hoc per library per API. You can easily forget to set one up, or call some library that didn’t, and it will get your thread stuck sooner or later.
For cancellation at application level you need custom solutions. So far I haven’t seen anything nicer than passing a context everywhere and regularly checking if ctx.cancelled() return.
I agree with that. That’s why I mentioned it in the “Async/await has good parts” section. I do feel like most arguments for async/await boil down to this specific point. So it is up to the programmer to decide if async/await’ing the entire codebase is worth it to get ergonomic timeouts and (somewhat) ergonomic cancellation.
You don’t need to async/await the entire codebase. You only need to do so for the parts of your program that deal with IO and orchestration.
Most of nextest is synchronous – there’s just really one file (the core runner loop) that’s async. For example, nextest’s config parsing code is completely synchronous.
I ran into this issue once and it took me an entire day to figure out. The kind of bug that only happens to you once. I am very careful with locks around if lets now :)
A nonempty source file does not end in a new-line character which is not immediately preceded by a backslash character or ends in a partial preprocessing token or comment (5.2.1.2).
LLMs often get way too much credit for what they produce. If you look closely, most of the thinking was already done by the human entering the prompt. For example: Let’s say you are writing an email to someone asking them for help. Maybe you’ll prompt ChatGPT some personal detail that it should use. And you write a short summary of the problem you need help with. ChatGPT will write up a real nice email. But if you think about it, what is real impressive is the human identifying an expert that has the right knowledge to help with the problem (even more so, choosing the option to seek help at all, if you ask Copilot to write you an algorithm it won’t respond “ask your colleague Dan for help, he will know”), and the human identifying that one personal detail to make them more likely to cooperate. Actually, the human did all the work. ChatGPT just converts it to email form basically. Anyway, this article shows beautifully that LLMs will use the provided context even when it doesn’t make sense. Question is if an LLM could come up with real world solutions if it had all the context a human has.
In the ‘inline fn hasFeature’ example towards the end, does elision happen because it is guaranteed by comptime, or because of optimization by the compiler? Sometimes with Zig it is hard to see where comptime ends and optimization begins…
In Zig, inline is not an optimization; it does the inlining in the frontend, as if you had copy-pasted the function body at the callsite. In my opinion, this is easier to reason about than the inline hints you get in other programming languages, where it may or may not do something.
I actually meant the elision in the if statement itself (reading it back now I my comment was a bit vague). In the example, the branch is elided even though only the first part of the expression is comptime. I.e. the if statement is “if (comptime X and Y)”. The zig reference says that the branch is guaranteed to be removed if the expression is comptime. But here only part of it is. So my guess is that the compiler removed the branch since it became “if (false and Y)”. Hope this makes sense.
Your guess is correct; it’s not optimization, it’s guaranteed. A similar case can be demonstrated: if you have a function which says this:
fn main() void {
// …
if (false and xyz()) {}
}
fn xyz() bool {
@compileError("nope");
}
it’ll build successfully. If you change the false to true, you’ll get a “nope”! A comptime-known bool will shortcircuit at compile time, in the same way that the comptime known expression given to if will cause conditional compilation of its body.
Hmmm. So that doesn’t happen by default? That’s unfortunate. Does this result in a function-coloring-like effect in practice, where you end up implementing the same logic in both comp time and non-comptime contexts?
Am I the only one who is bothered that the font size in the code examples seems to vary randomly, e.g. the “throw” line in the first example is larger than the other lines? I have seen this in some other blogs as well, recently.
Are you on iOS? I’ve been seeing this quite often in code samples on my iPhone. But never on desktop (Firefox). My guess is that there’s some “smart” readability feature on Safari that is messing things up.
Except for enum methods, I don’t know what this is about either.
And that it’s critical switch be called switch for familiarity with C, as demonstrated by a snippet which looks nothing like C except for having the keyword switch, which does not even remotely behave like it does in C.
In the conclusion it claims that Swift is better for writing operating systems…
I think you read that backwards.
Rust is better for systems and embedded programming. It’s better for writing compilers and browser engines (Servo) and it’s better for writing entire operating systems.
In the paragraph after it says “Swift is better for writing UI and servers and some parts of compilers and operating systems.”
So maybe a bit unfair of me to characterize it that way, but what does the author mean by saying Rust is better for writing an entire OS, Swift for “some parts” of an OS.
This article is very interesting and touches on some of the pain points with async Rust. It got me thinking I should probably use one of those share nothing runtimes instead of Tokio. Either way, this a many other articles lately have made me unsure if Async Rust may have been a mistake. It has so many of these sharp corners.
It has a lot of sharp corners for sure. But also, what we had before (futures 0.1 without first-class language support for async) was much worse. It was complete future adapter hell, and the compiler errors were nearly useless. Today’s async rust is a much better experience overall.
It’s still a worse time than non-async Rust, sure. And that option is still there if you don’t need async, it was never going anywhere.
I don’t think async Rust is a mistake (far from it), but I do think that people have sort of defaulted to “I’m doing many things at once, let’s use async”, even though there’s whole classes of problems where that generates a bunch of pain points. But in a “let’s just do some message passing inside our app” model, you can live very comfortably and have way less to worry about.
I think it’s not really insisted on enough that async/await is very good for work where you want arbitrary suspension points. Even for things like webservers, do you really want arbitrary suspension points? Or do you just want multi-threading?
(I think the nuanced answer for webservers is that if you find that you are making a lot of I/O in the middle of your requests, you probably do want async/await to some extent. But even then, if you’re an RDBMS transaction user you’re likely going to have a fixed max number of workers anyways and that might not even give you much benefit! Your underlying I/O needs to be parallelizable!)
I’m saying this but of course if you have your setup all good, async/await is aesthetically pleasing.
I think you’re just studying the corners where things could potentially be improved. If you ever worked with async rust pre async-await, then you will surely know how much better it already is. And while I started with sync rust (there was no future), I wouldn’t want to write most of my current async code in sync. Way too much manual state machinery for that.
Either way, this a many other articles lately have made me unsure if Async Rust may have been a mistake. It has so many of these sharp corners.
This isn’t a sharp corner with async rust. What’s in the language itself is pretty agnostic to these things. And at least for the Send and Sync bounds, it wouldn’t have mattered how you designed it, if you want to do work stealing you’d need those bounds. Maybe you don’t want to do work stealing, but again, that’s definitely an option already, the design of async Rust isn’t married to work stealing.
Technically it’s an option already, But in practice if you go that route then you are going to have to write most of the supporting libraries you would normally just get from crates.io because as the article points out. Library authors know that their ecosystem is tokio and with the current state of async traits and such they default to requiring all futures they consume or produce to be Send + 'static. The dominance of tokio in the ecosystem means in practice it’s just all around easier to support work stealing runtimes and wrap everything in Arc and Mutex instead of trying to swim against that stream.
But in practice if you go that route then you are going to have to write most of the supporting libraries you would normally just get from crates.io
That’s going to happen anyway – most of the packages on crates.io aren’t of high quality, so if your project has performance or correctness requirements then writing custom libraries becomes the default approach. And every time a high-quality Rust library can’t be published on crates.io for policy reasons (e.g. flat namespace), that difference in average quality grows a bit bigger.
And every time a high-quality Rust library can’t be published on crates.io for policy reasons (e.g. flat namespace)
Can you elaborate on that? Are you saying people/companies are not publishing on crates.io because their software’s name is taken already? Do you have examples of that happening?
I don’t have any examples I can share publicly, but I can describe a hypothetical that may or may not have happened, and then gesture vaguely in the direction of what the happy path looks like for other languages.
Say you’re working at a tech company and need to write a PAM module, which is a shared library that gets linked into all sorts of sensitive processes. You have the choice of writing it in C (with libpam) or in Rust. You choose Rust because you like the idea of not getting paged at 3 AM for sshd segfaults. You notice there is a pam crate, but it’s clearly incomplete and has a lot of dubious-looking unsafe blocks, so you decide the pager will be quieter if you write a new PAM library for Rust from scratch.
A few weeks later your project has gone well, the new code’s running, and you’ve got this //depot/hooli3/security/pam/rust/pam.rs file sitting around just begging to be open-sourced for that sweet sweet quarterly accomplishments bullet point. You start working through the process and hit a snag, because you can’t upload your pam crate to crates.io. There is already a pam crate. Computer says no.
You ask for guidance in the Rust IRC Zulip, and a crates.io administrator says that naming libraries after their purpose is boring. They suggest giving your project a whimsical codename – for example the name cooking-spray is available! A link to why’s Poignant Guide to Ruby is helpfully provided as inspiration. You thank them for their advice and close the tab, with a feeling that you’d rather abandon civilization and live in the forest eating wild tubers than publish a PAM library named cooking-spray.
Meanwhile, in the world of Go programming, there is no central registry and nobody to tell you what your library’s allowed to be called. All Go libraries are equal in the eyes of HTTP. So the list of Go libraries published by big tech companies includes names like this:
You can’t upload your company’s DNS library to crates.io because that name is taken by a mostly-empty v0.0.1 crate uploaded 10 years ago (https://crates.io/crates/dns), but if it was written in Go you could publish it to the world with git push and still have time to swing by the soft-serve machine before your next meeting.
And it’s not just Go. Many languages don’t have a centralized package registry with a flat namespace, and low-level libraries in those languages tend to use plain names too:
The crates.io package naming policy is just uniquely out of sync with the needs of the kind of people who get paid to write low-level Rust code.
I could speculate on the exact cultural reasons why people with a systems programming background name their libraries for their functionality and people with a web design background name for branding and surrealism, and how that mindset affects their beliefs about the viability of a single flat namespace for library names, but this comment is already longer than some blog posts.
It’s important to note that you can upload the package as google-uuid to crates.io, the users would have to spell it as google-uuid = "1.0" in their Cargo.toml, but they’ll still write use uuid; in their .rs files.
lib.name filed in Cargo.toml exists, and you can use it if it’s important that the source-level name is boring and direct.
That seems like the worst of all options – the library name wouldn’t match what people see in their source code, and there’s no way to tell which parts of the package name are meaningful other than guessing.
How about calling it google-uuid and having users import it as google_uuid? I don’t see much downside to google-uuid relative to https://github.com/google/uuid, except that the latter hard-enforces that you own that domain, while the former only soft-enforces that this crate is made by Google.
It sounds like you might be under the impression that, because a crates.io administrator expressed an opinion on crate naming in Zulip, that you have to name your crates that way. I don’t see why that’s true. Just call it google-uuid and upload it. I can’t imagine they would do anything as dramatic as taking your crate down, but if they do then I’ll join you with my pitchfork.
(Tangent: yes, there’s a mismatch between - and _ and it bothers me too. Your choice is either to go with the convention for - in Cargo but _ in Rust, or to go against that convention and use _ for both. Pick your poison.)
I’ll be more explicit, since it sounds like you might have misunderstood my position.
A library dealing with UUIDs should be named uuid.
A library dealing with some Google-specific variant of UUIDs should be named google_uuid.
If Google wants to publish a UUID library, they shouldn’t have to call it google_uuid – just normal plain uuid should be good enough.
Google and Joe Smith should both be allowed to publish libraries named uuid on crates.io.
It is the preference of crates.io administrators to not support package namespaces, so currently that is not possible. There can be only one library named uuid on crates.io, because the package name and library name are conflated.
Given a crates.io package that contains a library, it should be possible to extract the name of the library from the name of the package.
Goofy schemes like “prefix your package names with jdoe- and set lib.name” don’t work well, there’s no way to separate the library name from the package name without actually downloading the thing and parsing Cargo.toml.
It might be easier to think in terms of symbol namespaces in code. Right now crates.io is the equivalent of C, where all the functions from every included header share the same namespace. I would like the equivalent of C++‘s namespace { ... } or Rust’s mod { ... }, where symbols with the same name can be kept separate and given a less ambiguous global identifier.
One of the things that annoys me about the discussion is that the crates.io admins reject aesthetically-pleasing namespacing schemes on technical grounds, then reject the technically plausible schemes on aesthetic grounds.
GitHub usernames like ~jdoe/uuid are out because GitHub usernames can change, which causes support load – unfortunate, but fair.
Go-style domain names are out because they can change ownership if the domain lapses, which might be a security issue in a world where everyone auto-updates dependencies. Less convinced this is true in reality, but sure, fine, whatever.
Numeric user IDs like ~12345/uuid rejected for making it difficult to transfer package ownership, which would mean the original owner couldn’t transfer and then re-create the package. Seems like it’d be fine to say “just don’t do that”? But okay, let’s go with it, no numeric IDs.
Randomly-generated per-package IDs like 49bf3d2f38df4439b3f9a04668ba17d9/uuid rejected for being ugly, but what’s the alternative? And does anyone actually care?
If someone uploaded uuid ten years ago and I want to register my uuid today, I don’t care if the first person gets to keep the short un-namespaced package name. It’s better than being completely blocked, and it’s not like I’m going to be typing in package names anywhere.
The Microsoft ecosystem has been using UUIDs to identify software dependencies for like twenty years, it works fine.
I’m also generally skeptical of the argument that some crates.io admins have used that namespaces are unnecessary because NPM and PyPI don’t have them, but that’s part of the reason NPM and PyPI are so infamously bad! Rust should be trying to learn from mistakes, not follow them out of tradition.
What are some actual problems caused by publishing under google-uuid instead of https://github.com/google/uuid? I mean concrete problems, that go beyond “it mixes the namespace together with the library name, which are different concepts in my head”.
(As a human, when I see google-uuid I can parse that this is a uuid library published by Google. My impression is that crates.io is pretty low touch, but I imagine that if someone who wasn’t google published a library called google-uuid you could get them to take it down for flagrant misrepresentation. As a user, I need to use google_uuid as uuid; in my Rust code, which is… fine. The only real downside I can imagine is that machines can’t separate the namespace google from the library uuid, but I don’t know why that’s important.)
Why couldn’t I publish a crate named google-uuid? There’s currently no policy against it, and there’s a whole ton of crates published by random people that implement wrappers around various Google APIs. For example:
So if you want to see who published a crate, you need to go look at the authors list instead of just looking at the crate name. That… doesn’t seem like a huge deal?
(Note that if I were to design a package manager, it would probably work like Go’s instead of like Rust’s. I was wondering if there were reasons against having a flat namespace that I wasn’t aware of. Seems like no, you just have strong opinions about them. I’ll stop this thread here, doesn’t seem like continuing will reveal any new information.)
You’re missing the important parts and focusing on irrelevant details.
crates.io has no package namespaces, so only one library named uuid can be registered on crates.io at a time.
Trying to simulate package namespaces with library name prefixes doesn’t work, because any library name prefix that would be usable as a pseudo-namespace are already valid in existing library names.
If a namespace is associated with a person, then controlling which packages can be put into one’s own namespace is an important feature for any publishing platform, including package registries. People would not use GitHub if other people could put repositories under their username. Similar statements apply to large projects with many packages, and to organizations.
crates.io has no way to restrict access to specific library name pseudo-prefixes and no plausible way to do so, which is why library name pseudo-prefixes are not suitable as an alternative to package namespaces associated with people, projects, or organizations.
The crates.io policy against package namespaces only makes sense to people who believe libraries should be given individual, distinct, and memorable names. This belief is not common among systems programmers, who are the primary audience of the Rust language.
Randomly-generated per-package IDs like 49bf3d2f38df4439b3f9a04668ba17d9/uuid rejected for being ugly, but what’s the alternative? And does anyone actually care? … The Microsoft ecosystem has been using UUIDs to identify software dependencies for like twenty years, it works fine.
UUIDs sound good to me too. If they sound fine to you too, to ahead and publish “uuid_49bf3d2f38df4439b3f9a04668ba17d9”. I’m pretty sure there’s no rule against it.
Appreciate the response. First of all, I fully agree with you. I’ve complained about cargo namespace for as long as I can remember.
You ask for guidance in the Rust IRC Zulip, and a crates.io administrator says that naming libraries after their purpose is boring.
Yeah, that checks out. Conversations around this issue has hit a dead-end. The people in charge have a preference, and that’s the end of it.
The crates.io package naming policy is just uniquely out of sync with the needs of the kind of people who get paid to write low-level Rust code.
I actually never looked at it that way, but thinking about it, you’re 1000% correct. Especially for highly technical libraries that relate to hardware or protocols. If there are 3 competing Ethernet drivers, I’d prefer 3 identically named cxgbe crated instead of random non-sensical words.
I think you vastly underestimate the willingness of projects/companies, even ones using rust, to use a crates.io library regardless of the quality or performance. You can go quite far before it becomes a problem you have to solve.
This is one of the reasons I try not to use “use”/“import” much. It makes grepping through the codebase easier, and also makes it much easier to refactor code ie lift out a part of the code to a new module. And finally it makes it easier to see where a type/function is coming from.
Same here. I learned about dynamic linking on Windows first and this post helped understand the differences between how it’s done on Linux:
Windows: single linker that is a part of the system (maybe even the kernel).
Linux (or maybe the correct term here is “ELF-based systems”): user mode facility bundled together with libc. glibc has one, musl libc has its own too.
The atomic stop bool in the code example is a bit of a distraction. In the example, the author sets the stop flag and then immediately terminates the runtime. You cannot expect the stop semantics to work if you don’t wait for the spawned tasks to complete. There is no point to that. So, if we think the stop flag away, what remains is the insight that spawned tasks can be terminated in any order which does not feel as much as a footgun to me.
The point of the stop flag is to actually show what kind of (real, non-abstract) thing could break as a result of the unspecified ordering, which is indeed the only bold sentence on the page:
The Tokio runtime will drop tasks in an arbitrary order during shutdown.
If you wrote the same code without the stop flag, you’d probably handle this correctly by intuition, writing each task in awareness that it is just sitting there in a big sea of runtime, hoping someone else can hear them — if not, when you hit the interrupt, you’ll likely find out.
But if you design it with the stop flag first, you might be thinking and planning more for the ‘expected’ shutdown sequence, and therefore neglect the case where the runtime is dropped instead, causing a bit of an unscheduled disassembly.
So I think that insight is the only one the page is trying to convey, but the stop flag isn’t a distraction, it’s motivating that insight.
The reason the stop flag does not work is because the runtime is shut down before it can even do its work. That’s the reason it’s broken, doesn’t really have anything to do with the order of when tasks are dropped. If the authors suggested solution (to drop everything after stopping all the tasks) were applied here then it would happen to solve this specific manifestation of the underlying bug, but the bug remains. If the receiver task for example would have some other cleanup code inside the if statement then that may never execute even though apparently that is what the author intended.
thinking and planning more for the ‘expected’ shutdown sequence
True–but there’s no actual explicit shutdown sequence present. In order for the tasks to shutdown cleanly, the author should be waiting for tasks to clean themselves up once signalled (and you’d need a mechanism that would actually notify the task, too).
It seems that they’re assuming that the runtime will run the tasks to completion; which could block indefinitely, and just as non-obvious.
It seems that they’re assuming that the runtime will run the tasks to completion; which could block indefinitely, and just as non-obvious.
I wonder if that is their experience? I’ve had so many quirks with tokio getting to the end of my async main and then just… stalling. I don’t know why, I assume some task is being blocked, possibly by an I/O operation. It’s always whenever I’m doing something with networking and don’t take the absolutely utmost care to isolate and control the connection to make the async runtime happy. But in some of my prototypes I had to literally put a std::process::exit() at the end of my async main because it would get to that point and then just keep running. I never figured out exactly what causes it, it just happens.
Rust is getting too complex as it is. This feature aims to solve the issue of cloning into closures, which admittedly can get quite verbose. It’s a good solution, and that’s why it’s hard to argue against it. The long term problem is that adding every feature that makes sense will eventually have Rust end up like C++.
For real. As much as I do like TypeScript, its value is realized when in larger projects where contracts across code are needed because of the footguns you can run into JavaScript. But even then, modern JavaScript is good enough (IMO!) such that YAGNI TypeScript.
Different strokes, I guess. I wouldn’t write anything nontrivial in pure JS; it’s far too easy to misspell something or pass the wrong arg type or get args in the wrong order, and then not find out until runtime and have to debug a dumb mistake TS would have flagged the moment I typed it.
ha wild that typescript is the controversial part here. i haven’t encountered anyone advocating for full-stack javascript in years.
i think the overhead that defensive programming adds when using javascript justifies the added build process/tooling of typescript in anything but smaller scripts.
I’ve been building a medium-sized internal tool for my website, and I’ve debated many times whether I should switch to TypeScript for the superior IDE code analysis.
I chose not to, because JavaScript is good enough, and I really don’t wanna pull in the complexity of JavaScript build systems into my codebase. I really like that I can do cargo run and watch it go, without having to deal with npm or anything.
I’m sure you’re aware of this already, but just in case: have you tried using JSDoc-flavoured Typescript? You can write pretty much all Typescript types as JSDoc comments in a regular JS file. That way you get all the code analysis you want from your IDE (or even from the Typescript type checker using something like tsc --noEmit --allowJs), but you don’t need a separate build step. The result is typically more verbose than conventional Typescript, but for simple scripts it should work really well. I know the Svelte JS framework have gone down this route for various reasons — if you search for that, there might be some useful resources there.
I’ve recently started using a small amount of JSDoc for IDE completion, yeah. But I have written a small Lua codebase using this approach, and I can’t say I’m a huge fan; it gets old pretty quick.
I obviously can’t speak for all the hyperscalers, but a lot of folks at AWS sure are using
async/await, increasingly alongsideio_uringto great effect. There’s always money in performance, of course, but one of the great things about the Rust ecosystem is how easy it makes it to get to “really very good” performance.As a concrete example, when we were building AWS Lambda’s container cache, we build a prototype with
hyperandreqwestandtokiotalking regular HTTP. We totally expected to need to replace it with something more custom, but found that even the prototype was saturating 50Gb/s NICs and hitting our latency and tail latency targets, so just left them as-is.I think the reality is that, for high-performance high-request-rate applications, a custom scheduler can do much better than Linux’s general purpose scheduler without a whole lot of effort (mostly because it just knows more about the workload and its goals).
This doesn’t seem right either.
Do you think you would have been able to saturate the NIC using the traditional OS threads model (no async/await)?
Yeah, for sure. But the
asyncmodel made the code clearer and simpler then the equivalent threaded code would have been, when taking everything into account (especially the need to avoid metastability problems under overload, which generally precludes a naive thread-per-request implementation).I do appreciate the tail latency angle. There are some (synthetic) benchmarks that show async/await being superior here. (Of course this depends on the async runtime too.) On the other hand, it seems to me a bit too niche requirement to justify async/await, assuming overload is not a very common situation. I am assuming async/await being a worse experience here. And reading from your comment you did not have that experience.
Services of all sizes need to protect against overload. My understanding is that async/await enables the server to handle as much load as the CPU(s) can handle without having to tune arbitrary numbers (e.g. thread pool size), and allowing the network stack to apply natural backpressure if the load increases beyond that point.
Edit to add: If it seems that designing a service to gracefully handle or prevent overload is a niche concern, perhaps that’s because we tend to throw more hardware at our services than they really ought to need. Or maybe you’ve been lucky enough that you haven’t yet made a mistake in your client software that caused an avoidable overload of your service. I’ve done that, working on a SaaS application for a tiny company.
When using tokio (and this goes for most async runtimes) it is actually not recommended to use async/await for CPU-bound workloads. The docs recommend using
spawn_blockingwhich ends up in a thread pool with a fixed size: https://docs.rs/tokio/latest/tokio/#cpu-bound-tasks-and-blocking-codeNo, that’s about hogging up a bunch of CPU time without yielding. It doesn’t apply if you have tasks that use a lot of CPU in total but yield often, or simply have so many tasks that you saturate your CPU. I’m pretty sure the latter is what @mwcampbell was referring to with “enables the server to handle as much load as the CPU(s) can handle”.
Tail latency is extremely important and undervalued. This is why GC languages are unpopular in the limit, for example — managing tail latencies under memory pressure is very difficult.
edit: of all groups of lay people, I think gamers have come to understand this the best. Gamers are quite rightly obsessed with what they call “1% lows” and “0.1% lows”.
As an AWS user, I can say you can saturate S3 get objects calls with async await pretty easily as well, to the point where there’s a few github issues about it. https://github.com/awslabs/aws-sdk-rust/issues/1136 <– essentially you have to hold your concurrency to between 50-200 depending on where you’re situated wrt s3.
Like many (most?) posts that are labelled as being against async/await in Rust, this one seems to actually be against Tokio:
and
and
and
and
Tokio! Tokio! And more Tokio!
Reading slightly between the lines, the author seems to have started out with the assumption that they need an N:M userspace thread runtime for good I/O performance (because of a background in C# ?), then they found Tokio (an N:M userspace thread library that uses
asyncas one part of its implementation), and they’re having trouble getting Tokio to deliver on what the author expects of it.Maybe that’s the author’s fault, maybe it’s Tokio’s fault; I haven’t looked at the author’s code and therefore can’t judge either way. But it seems clear that it’s not Rust‘s fault, because as the author notes the
async/awaitmodel works great for (1) embedded environments and (2) multiplexing I/O operations on a single thread, which are exactly the use cases that Rust’sasync/awaitis designed to solve.Maybe the answer is for the Rust project to intentionally de-emphasize Tokio in its async/await documentation? Over and over it seems that every time I try to use Tokio in my own projects I stumble into weird and non-Rustic behavior (e.g. the implicit spilling to the heap mentioned in this post), and every time I see an experienced programmer struggling with async/await the problems all seem to revolve around Tokio in some capacity.
I doubt that would meaningfully help.
From my perspective, it’s down to a “Nobody ever got fired for choosing IBM” attitude around Tokio, stemming from “I can trust that every dependency I might need supports Tokio. I don’t want to slam face-first into the hazard of some required dependency not supporting async-std or smol or what have you”.
I think we’re just going to have to
await😜 more things like “async fnin traits” (Rust 1.75) landing as building blocks for looser coupling between dependencies and runtimes.(Also, Ugh. Lobste.rs apparently doesn’t have an
onbeforeunloadhandler for un-submitted posts and I accidentally closed the tab containing the previous draft of this after getting too comfortable to compose it in a separate text editor and then paste it over.)No, it’s because the existing library ecosystem is centered around tokio. If you’re not writing things yourself, you’re probably going to be using tokio. It’s the unofficial official Rust runtime.
I’ve heard this a lot, but it just doesn’t seem to be true – Tokio is popular but by no means universal, and it’s silly to act as if it’s somehow more official than (for example)
embassyorglommio.Most Rust libraries are written for synchronous operation. It’s a small minority that use
asyncat all, and even fewer of those hardcode the async runtime to Tokio. Not to mention that the use cases for which Rust has a clear advantage over Go/Java/C#/etc are things like embedded, WebAssembly, or dynamic libraries – none of which are supported by heavy N:M runtimes such as Tokio.To me tokio is a bunch of decisions made for me. At first when I saw it I disagreed with most of them in some way or another. After I couldn’t avoid it, I realized in the end this isn’t a half bad way of providing parallelism and for instance the alternative to “spilling to the heap” is essentially crashing.
What I think is scary about tokio for new users, and I’m planning a little post on, is that if you start going deeply async you end up unbounded, possibly with an explosion in the number of tasks depending on how your code is written. You can hit external limits, etc. Controlling that can only be done (afaict) with a tuned Arc limit from the root passing down to the overspawned task. To me it’s a small price for how easy writing and maintaining it is.
Some of the issues are Tokio specific, some are not. Either way thinking about async/await on purely a language-level is not helpful. Everyone has to pick a runtime and due to lock-in a majority will end up with tokio. Whether the issue stems from Tokio or Rust’s language design ultimately does not matter to me as a programmer that wants my code to work.
But you frame the article as a general critique against async/await, saying htat most of what you say should apply even to other languages!
Are you aware that one hyperscaler, Cloudflare, replaced Nginx with an in-house proxy written in Rust using Tokio, in part due to issues with tail latencies and uneven load balancing between cores?
AWS’s core orchestration loop uses Tokio as well.
Meta’s C++ uses Folly coroutines extensively as something quite similar to async/await.
Yes (link here for those interested: https://blog.cloudflare.com/how-we-built-pingora-the-proxy-that-connects-cloudflare-to-the-internet/). My reading is that the performance issues were due to nginx not reusing connections across cores, which could be solved without Tokio. Cloudflare could have opted to use
miodirectly for example. On the other hand I do understand their choice to use Tokio here because it probably helped them ship much faster. I would not be surprised if they eventually swap out Tokio for a custom runtime (or maybe even use mio directly) since there would probably be some extra performance to be gained.Note that I edited my blog post a bit to reflect the fact that hyperscalers are using async/await.
I’ve looked at using mio directly. It’s much more difficult than using Tokio.
That’s not entirely correct. They also had difficulty with nginx’s thread-per-core model which precludes work-stealing.
Why would they use mio directly and implement a work stealing scheduler atop of Mio? To me, this seems like Tokio, but with extra steps.
I would be. A work-stealing scheduler is almost certainly what most people want/need, but it is especially optimal for load balancers/proxies.
I know you edited this, but the alternative to using async/await is pervasive, feral concurrency control where people constantly spin up new thread pools. async/await, in Rust, is substantially more efficient than the alternatives that people would otherwise gravitate to.
Rust async/await essentially is Tokio. I have yet to see any code in the wild, or any libraries, which use async/await but not tokio.
It’s extremely easy to find async code that doesn’t use Tokio. And in any case, this article claims to be a general criticism of the whole async/await paradigm, regardless of runtime or language.
The post is unfortunately very obviously written by somebody who never had a network filesystem hang up on them in their life.
Async/await can help with that. But seems a bit of a stretch to present it as if it were the only possible solution.
The other solutions end up looking very similar to async, but without the syntax sugar to make the code linear.
Timeouts and cancellation are important for reliability, but unfortunately the sync APIs we have suck at that. They’re all forever-blocking non-cancellable by default. Timeouts are ad-hoc per library per API. You can easily forget to set one up, or call some library that didn’t, and it will get your thread stuck sooner or later.
For cancellation at application level you need custom solutions. So far I haven’t seen anything nicer than passing a context everywhere and regularly checking
if ctx.cancelled() return.I agree with that. That’s why I mentioned it in the “Async/await has good parts” section. I do feel like most arguments for async/await boil down to this specific point. So it is up to the programmer to decide if async/await’ing the entire codebase is worth it to get ergonomic timeouts and (somewhat) ergonomic cancellation.
You don’t need to async/await the entire codebase. You only need to do so for the parts of your program that deal with IO and orchestration.
Most of nextest is synchronous – there’s just really one file (the core runner loop) that’s async. For example, nextest’s config parsing code is completely synchronous.
I ran into this issue once and it took me an entire day to figure out. The kind of bug that only happens to you once. I am very careful with locks around if lets now :)
C23 has “a comprehensive approach for program failure”. Can anybody provide a reference to this?
I think they mean it was added to the book, not the language.
Yeah I think this is referring to the new chapter 15 titled Program Failure.
Annex J.2? 🤪
Is that a typo? Why the double negative?
It’s not a typo; it’s saying that a source file must end in a new-line character, and that new-line must not be immediately preceded by a backslash.
This is what I hate about the C standard—the language (in my opinion) is very tortured at points and hard to read.
LLMs often get way too much credit for what they produce. If you look closely, most of the thinking was already done by the human entering the prompt. For example: Let’s say you are writing an email to someone asking them for help. Maybe you’ll prompt ChatGPT some personal detail that it should use. And you write a short summary of the problem you need help with. ChatGPT will write up a real nice email. But if you think about it, what is real impressive is the human identifying an expert that has the right knowledge to help with the problem (even more so, choosing the option to seek help at all, if you ask Copilot to write you an algorithm it won’t respond “ask your colleague Dan for help, he will know”), and the human identifying that one personal detail to make them more likely to cooperate. Actually, the human did all the work. ChatGPT just converts it to email form basically. Anyway, this article shows beautifully that LLMs will use the provided context even when it doesn’t make sense. Question is if an LLM could come up with real world solutions if it had all the context a human has.
In the ‘inline fn hasFeature’ example towards the end, does elision happen because it is guaranteed by comptime, or because of optimization by the compiler? Sometimes with Zig it is hard to see where comptime ends and optimization begins…
In Zig,
inlineis not an optimization; it does the inlining in the frontend, as if you had copy-pasted the function body at the callsite. In my opinion, this is easier to reason about than the inline hints you get in other programming languages, where it may or may not do something.The downside is that it gets overused, to the point where I had to warn against using it in the language reference.
I actually meant the elision in the if statement itself (reading it back now I my comment was a bit vague). In the example, the branch is elided even though only the first part of the expression is comptime. I.e. the if statement is “if (comptime X and Y)”. The zig reference says that the branch is guaranteed to be removed if the expression is comptime. But here only part of it is. So my guess is that the compiler removed the branch since it became “if (false and Y)”. Hope this makes sense.
Your guess is correct; it’s not optimization, it’s guaranteed. A similar case can be demonstrated: if you have a function which says this:
it’ll build successfully. If you change the
falsetotrue, you’ll get a “nope”! A comptime-known bool will shortcircuit at compile time, in the same way that the comptime known expression given toifwill cause conditional compilation of its body.Thanks, that is helpful :)
Does it make sense to have a caller-side inline? I feel it can make sense to want a function to be inlined only at certain callsites.
The
@call()builtin function supports caller-side inline! To call functionfooand inline at the call site you useTo add to what Andrew said, this is quoted from Andrew’s link to the langref as one of the reasons to use
inline:Hmmm. So that doesn’t happen by default? That’s unfortunate. Does this result in a function-coloring-like effect in practice, where you end up implementing the same logic in both comp time and non-comptime contexts?
inline fnhas semantic implications so it’s not just an optimization.https://ziglang.org/documentation/master/#inline-fn
Am I the only one who is bothered that the font size in the code examples seems to vary randomly, e.g. the “throw” line in the first example is larger than the other lines? I have seen this in some other blogs as well, recently.
Are you on iOS? I’ve been seeing this quite often in code samples on my iPhone. But never on desktop (Firefox). My guess is that there’s some “smart” readability feature on Safari that is messing things up.
I‘m on an iPhone and it happens both in Safari and Firefox. Truly weird.
I think the CSS needs to set these (can’t remember if both are required) for the preformatted code blocks to prevent this
Probably in a
codeorpreselectorWow, that is some interesting code indeed. Not sure if I’m impressed or terrified.
I was able to extract these reasons why Swift is more convenient than Rust from the article:
In the conclusion it claims that Swift is better for writing operating systems…
TFA basically equates explicit and inconvenient.
Except for enum methods, I don’t know what this is about either.
And that it’s critical
switchbe calledswitchfor familiarity with C, as demonstrated by a snippet which looks nothing like C except for having the keywordswitch, which does not even remotely behave like it does in C.I think you read that backwards.
In the paragraph after it says “Swift is better for writing UI and servers and some parts of compilers and operating systems.”
So maybe a bit unfair of me to characterize it that way, but what does the author mean by saying Rust is better for writing an entire OS, Swift for “some parts” of an OS.
This article is very interesting and touches on some of the pain points with async Rust. It got me thinking I should probably use one of those share nothing runtimes instead of Tokio. Either way, this a many other articles lately have made me unsure if Async Rust may have been a mistake. It has so many of these sharp corners.
It has a lot of sharp corners for sure. But also, what we had before (futures 0.1 without first-class language support for async) was much worse. It was complete future adapter hell, and the compiler errors were nearly useless. Today’s async rust is a much better experience overall.
It’s still a worse time than non-async Rust, sure. And that option is still there if you don’t need async, it was never going anywhere.
I don’t think async Rust is a mistake (far from it), but I do think that people have sort of defaulted to “I’m doing many things at once, let’s use
async”, even though there’s whole classes of problems where that generates a bunch of pain points. But in a “let’s just do some message passing inside our app” model, you can live very comfortably and have way less to worry about.I think it’s not really insisted on enough that
async/awaitis very good for work where you want arbitrary suspension points. Even for things like webservers, do you really want arbitrary suspension points? Or do you just want multi-threading?(I think the nuanced answer for webservers is that if you find that you are making a lot of I/O in the middle of your requests, you probably do want async/await to some extent. But even then, if you’re an RDBMS transaction user you’re likely going to have a fixed max number of workers anyways and that might not even give you much benefit! Your underlying I/O needs to be parallelizable!)
I’m saying this but of course if you have your setup all good,
async/awaitis aesthetically pleasing.I think you’re just studying the corners where things could potentially be improved. If you ever worked with async rust pre async-await, then you will surely know how much better it already is. And while I started with sync rust (there was no
future), I wouldn’t want to write most of my current async code in sync. Way too much manual state machinery for that.This isn’t a sharp corner with async rust. What’s in the language itself is pretty agnostic to these things. And at least for the Send and Sync bounds, it wouldn’t have mattered how you designed it, if you want to do work stealing you’d need those bounds. Maybe you don’t want to do work stealing, but again, that’s definitely an option already, the design of async Rust isn’t married to work stealing.
Technically it’s an option already, But in practice if you go that route then you are going to have to write most of the supporting libraries you would normally just get from crates.io because as the article points out. Library authors know that their ecosystem is tokio and with the current state of async traits and such they default to requiring all futures they consume or produce to be
Send + 'static. The dominance of tokio in the ecosystem means in practice it’s just all around easier to support work stealing runtimes and wrap everything inArcandMutexinstead of trying to swim against that stream.That’s going to happen anyway – most of the packages on crates.io aren’t of high quality, so if your project has performance or correctness requirements then writing custom libraries becomes the default approach. And every time a high-quality Rust library can’t be published on crates.io for policy reasons (e.g. flat namespace), that difference in average quality grows a bit bigger.
Can you elaborate on that? Are you saying people/companies are not publishing on crates.io because their software’s name is taken already? Do you have examples of that happening?
I don’t have any examples I can share publicly, but I can describe a hypothetical that may or may not have happened, and then gesture vaguely in the direction of what the happy path looks like for other languages.
Say you’re working at a tech company and need to write a PAM module, which is a shared library that gets linked into all sorts of sensitive processes. You have the choice of writing it in C (with
libpam) or in Rust. You choose Rust because you like the idea of not getting paged at 3 AM for sshd segfaults. You notice there is apamcrate, but it’s clearly incomplete and has a lot of dubious-lookingunsafeblocks, so you decide the pager will be quieter if you write a new PAM library for Rust from scratch.A few weeks later your project has gone well, the new code’s running, and you’ve got this
//depot/hooli3/security/pam/rust/pam.rsfile sitting around just begging to be open-sourced for that sweet sweet quarterly accomplishments bullet point. You start working through the process and hit a snag, because you can’t upload yourpamcrate to crates.io. There is already apamcrate. Computer says no.You ask for guidance in the Rust
IRCZulip, and a crates.io administrator says that naming libraries after their purpose is boring. They suggest giving your project a whimsical codename – for example the namecooking-sprayis available! A link to why’s Poignant Guide to Ruby is helpfully provided as inspiration. You thank them for their advice and close the tab, with a feeling that you’d rather abandon civilization and live in the forest eating wild tubers than publish a PAM library namedcooking-spray.Meanwhile, in the world of Go programming, there is no central registry and nobody to tell you what your library’s allowed to be called. All Go libraries are equal in the eyes of HTTP. So the list of Go libraries published by big tech companies includes names like this:
You can’t upload your company’s DNS library to crates.io because that name is taken by a mostly-empty v0.0.1 crate uploaded 10 years ago (https://crates.io/crates/dns), but if it was written in Go you could publish it to the world with
git pushand still have time to swing by the soft-serve machine before your next meeting.And it’s not just Go. Many languages don’t have a centralized package registry with a flat namespace, and low-level libraries in those languages tend to use plain names too:
The crates.io package naming policy is just uniquely out of sync with the needs of the kind of people who get paid to write low-level Rust code.
I could speculate on the exact cultural reasons why people with a systems programming background name their libraries for their functionality and people with a web design background name for branding and surrealism, and how that mindset affects their beliefs about the viability of a single flat namespace for library names, but this comment is already longer than some blog posts.
It’s important to note that you can upload the package as
google-uuidto crates.io, the users would have to spell it asgoogle-uuid = "1.0"in their Cargo.toml, but they’ll still writeuse uuid;in their .rs files.lib.namefiled in Cargo.toml exists, and you can use it if it’s important that the source-level name is boring and direct.That seems like the worst of all options – the library name wouldn’t match what people see in their source code, and there’s no way to tell which parts of the package name are meaningful other than guessing.
How about calling it
google-uuidand having users import it asgoogle_uuid? I don’t see much downside togoogle-uuidrelative tohttps://github.com/google/uuid, except that the latter hard-enforces that you own that domain, while the former only soft-enforces that this crate is made by Google.It sounds like you might be under the impression that, because a crates.io administrator expressed an opinion on crate naming in Zulip, that you have to name your crates that way. I don’t see why that’s true. Just call it
google-uuidand upload it. I can’t imagine they would do anything as dramatic as taking your crate down, but if they do then I’ll join you with my pitchfork.(Tangent: yes, there’s a mismatch between
-and_and it bothers me too. Your choice is either to go with the convention for-inCargobut_in Rust, or to go against that convention and use_for both. Pick your poison.)I’ll be more explicit, since it sounds like you might have misunderstood my position.
uuid.google_uuid.google_uuid– just normal plainuuidshould be good enough.uuidon crates.io.uuidon crates.io, because the package name and library name are conflated.jdoe-and setlib.name” don’t work well, there’s no way to separate the library name from the package name without actually downloading the thing and parsingCargo.toml.It might be easier to think in terms of symbol namespaces in code. Right now crates.io is the equivalent of C, where all the functions from every included header share the same namespace. I would like the equivalent of C++‘s
namespace { ... }or Rust’smod { ... }, where symbols with the same name can be kept separate and given a less ambiguous global identifier.One of the things that annoys me about the discussion is that the crates.io admins reject aesthetically-pleasing namespacing schemes on technical grounds, then reject the technically plausible schemes on aesthetic grounds.
~jdoe/uuidare out because GitHub usernames can change, which causes support load – unfortunate, but fair.~12345/uuidrejected for making it difficult to transfer package ownership, which would mean the original owner couldn’t transfer and then re-create the package. Seems like it’d be fine to say “just don’t do that”? But okay, let’s go with it, no numeric IDs.49bf3d2f38df4439b3f9a04668ba17d9/uuidrejected for being ugly, but what’s the alternative? And does anyone actually care?uuidten years ago and I want to register myuuidtoday, I don’t care if the first person gets to keep the short un-namespaced package name. It’s better than being completely blocked, and it’s not like I’m going to be typing in package names anywhere.I’m also generally skeptical of the argument that some crates.io admins have used that namespaces are unnecessary because NPM and PyPI don’t have them, but that’s part of the reason NPM and PyPI are so infamously bad! Rust should be trying to learn from mistakes, not follow them out of tradition.
What are some actual problems caused by publishing under
google-uuidinstead ofhttps://github.com/google/uuid? I mean concrete problems, that go beyond “it mixes the namespace together with the library name, which are different concepts in my head”.(As a human, when I see
google-uuidI can parse that this is auuidlibrary published by Google. My impression is thatcrates.iois pretty low touch, but I imagine that if someone who wasn’t google published a library calledgoogle-uuidyou could get them to take it down for flagrant misrepresentation. As a user, I need touse google_uuid as uuid;in my Rust code, which is… fine. The only real downside I can imagine is that machines can’t separate the namespacegooglefrom the libraryuuid, but I don’t know why that’s important.)Why couldn’t I publish a crate named
google-uuid? There’s currently no policy against it, and there’s a whole ton of crates published by random people that implement wrappers around various Google APIs. For example:Same for other companies:
Without looking, which of these four crates are maintained by Amazon, and which are just some bloke in a shed?
Is
julia-set-explorerpublished by someone named Julia, or is it related to the Julia programming language?https://crates.io/crates/libc is maintained by the Rust core developers. https://crates.io/crates/libr is not. Can you tell without looking which category https://crates.io/crates/libm is?
libmis at v0.2.8 and has the description “libm in pure Rust” – does that change the probability distribution of your guess?So if you want to see who published a crate, you need to go look at the authors list instead of just looking at the crate name. That… doesn’t seem like a huge deal?
(Note that if I were to design a package manager, it would probably work like Go’s instead of like Rust’s. I was wondering if there were reasons against having a flat namespace that I wasn’t aware of. Seems like no, you just have strong opinions about them. I’ll stop this thread here, doesn’t seem like continuing will reveal any new information.)
You’re missing the important parts and focusing on irrelevant details.
crates.io has no package namespaces, so only one library named
uuidcan be registered on crates.io at a time.Trying to simulate package namespaces with library name prefixes doesn’t work, because any library name prefix that would be usable as a pseudo-namespace are already valid in existing library names.
There is no way to determine which subset of a library name might be a pseudo-namespace, because
google-uuidcould be https://github.com/google/uuid or it could be https://github.com/jmillikin/google-uuid (or ~infinite other options). Examples of both patterns currently exist.If a namespace is associated with a person, then controlling which packages can be put into one’s own namespace is an important feature for any publishing platform, including package registries. People would not use GitHub if other people could put repositories under their username. Similar statements apply to large projects with many packages, and to organizations.
crates.io has no way to restrict access to specific library name pseudo-prefixes and no plausible way to do so, which is why library name pseudo-prefixes are not suitable as an alternative to package namespaces associated with people, projects, or organizations.
The crates.io policy against package namespaces only makes sense to people who believe libraries should be given individual, distinct, and memorable names. This belief is not common among systems programmers, who are the primary audience of the Rust language.
UUIDs sound good to me too. If they sound fine to you too, to ahead and publish “uuid_49bf3d2f38df4439b3f9a04668ba17d9”. I’m pretty sure there’s no rule against it.
I don’t want my code to contain
uuid_49bf3d2f38df4439b3f9a04668ba17d9::UUID.The package manager’s behavior shouldn’t cause changes in how the library itself is used by downstream code.
Appreciate the response. First of all, I fully agree with you. I’ve complained about cargo namespace for as long as I can remember.
Yeah, that checks out. Conversations around this issue has hit a dead-end. The people in charge have a preference, and that’s the end of it.
I actually never looked at it that way, but thinking about it, you’re 1000% correct. Especially for highly technical libraries that relate to hardware or protocols. If there are 3 competing Ethernet drivers, I’d prefer 3 identically named
cxgbecrated instead of random non-sensical words.I think you vastly underestimate the willingness of projects/companies, even ones using rust, to use a crates.io library regardless of the quality or performance. You can go quite far before it becomes a problem you have to solve.
This is one of the reasons I try not to use “use”/“import” much. It makes grepping through the codebase easier, and also makes it much easier to refactor code ie lift out a part of the code to a new module. And finally it makes it easier to see where a type/function is coming from.
I never fully understood what the whole glibc and interpreter stuff was all about, this article made it click for me :) Very interesting!
Same here. I learned about dynamic linking on Windows first and this post helped understand the differences between how it’s done on Linux:
The full summary on Wikipedia: https://en.wikipedia.org/wiki/Dynamic_linker
The atomic stop bool in the code example is a bit of a distraction. In the example, the author sets the stop flag and then immediately terminates the runtime. You cannot expect the stop semantics to work if you don’t wait for the spawned tasks to complete. There is no point to that. So, if we think the stop flag away, what remains is the insight that spawned tasks can be terminated in any order which does not feel as much as a footgun to me.
The point of the stop flag is to actually show what kind of (real, non-abstract) thing could break as a result of the unspecified ordering, which is indeed the only bold sentence on the page:
If you wrote the same code without the stop flag, you’d probably handle this correctly by intuition, writing each task in awareness that it is just sitting there in a big sea of runtime, hoping someone else can hear them — if not, when you hit the interrupt, you’ll likely find out.
But if you design it with the stop flag first, you might be thinking and planning more for the ‘expected’ shutdown sequence, and therefore neglect the case where the runtime is dropped instead, causing a bit of an unscheduled disassembly.
So I think that insight is the only one the page is trying to convey, but the stop flag isn’t a distraction, it’s motivating that insight.
The reason the stop flag does not work is because the runtime is shut down before it can even do its work. That’s the reason it’s broken, doesn’t really have anything to do with the order of when tasks are dropped. If the authors suggested solution (to drop everything after stopping all the tasks) were applied here then it would happen to solve this specific manifestation of the underlying bug, but the bug remains. If the receiver task for example would have some other cleanup code inside the if statement then that may never execute even though apparently that is what the author intended.
True–but there’s no actual explicit shutdown sequence present. In order for the tasks to shutdown cleanly, the author should be waiting for tasks to clean themselves up once signalled (and you’d need a mechanism that would actually notify the task, too).
It seems that they’re assuming that the runtime will run the tasks to completion; which could block indefinitely, and just as non-obvious.
I wonder if that is their experience? I’ve had so many quirks with tokio getting to the end of my async main and then just… stalling. I don’t know why, I assume some task is being blocked, possibly by an I/O operation. It’s always whenever I’m doing something with networking and don’t take the absolutely utmost care to isolate and control the connection to make the async runtime happy. But in some of my prototypes I had to literally put a
std::process::exit()at the end of my asyncmainbecause it would get to that point and then just keep running. I never figured out exactly what causes it, it just happens.Rust is getting too complex as it is. This feature aims to solve the issue of cloning into closures, which admittedly can get quite verbose. It’s a good solution, and that’s why it’s hard to argue against it. The long term problem is that adding every feature that makes sense will eventually have Rust end up like C++.
Choose one.
For real. As much as I do like TypeScript, its value is realized when in larger projects where contracts across code are needed because of the footguns you can run into JavaScript. But even then, modern JavaScript is good enough (IMO!) such that YAGNI TypeScript.
Different strokes, I guess. I wouldn’t write anything nontrivial in pure JS; it’s far too easy to misspell something or pass the wrong arg type or get args in the wrong order, and then not find out until runtime and have to debug a dumb mistake TS would have flagged the moment I typed it.
(Why yes, I am a C++/Rust programmer.)
ha wild that typescript is the controversial part here. i haven’t encountered anyone advocating for full-stack javascript in years.
i think the overhead that defensive programming adds when using javascript justifies the added build process/tooling of typescript in anything but smaller scripts.
There has been some talk about it recently, mostly started by DHH’s No Build blogpost
funny enough it is one of plainweb’s main principles to not have build processes (well almost, it uses esbuild until node can be replaced by bun)
especially frontend build processes are a major source of complexity that are imo not worth it for most web apps.
I’ve been building a medium-sized internal tool for my website, and I’ve debated many times whether I should switch to TypeScript for the superior IDE code analysis.
I chose not to, because JavaScript is good enough, and I really don’t wanna pull in the complexity of JavaScript build systems into my codebase. I really like that I can do
cargo runand watch it go, without having to deal with npm or anything.I’m sure you’re aware of this already, but just in case: have you tried using JSDoc-flavoured Typescript? You can write pretty much all Typescript types as JSDoc comments in a regular JS file. That way you get all the code analysis you want from your IDE (or even from the Typescript type checker using something like
tsc --noEmit --allowJs), but you don’t need a separate build step. The result is typically more verbose than conventional Typescript, but for simple scripts it should work really well. I know the Svelte JS framework have gone down this route for various reasons — if you search for that, there might be some useful resources there.I’ve recently started using a small amount of JSDoc for IDE completion, yeah. But I have written a small Lua codebase using this approach, and I can’t say I’m a huge fan; it gets old pretty quick.