I’ve been writing a new project using a microservices-based architecture; and during the development of the latest service; I realized that it needs to communicate with no less than 7 other microservices over HTTP (it also listens on two particular queues and publishes to two message queues).
During one iteration, it could potentially talk to all seven microservices depending on the logic path taken. As such, there is a lot of time spent talking over the network, and in a normal synchronous .NET Core application, there’s a lot of time spent blocking while this communication is happening. To resolve the blocking slowing down its responsiveness to the rest of the system, I ported it from being a synchronous to an asynchronous Microservice. This was a feat and took the better part of a day to do (for a Microservice, a day feels like a really long time). Along the way, I ran into several places where I have questions, but no answers (or at least no firm understanding as to whether or not my answer is right), and so I’ll post those questions here. You’ll find no answers here, only questions:
How far do I need to go down the asnyc rabbit hole?
If you’re writing a .NET Core Microservice, chances are you’re doing JSON serialization/deserialization. Since JSON.NET doesn’t have Async, our options are to leave it synchronous in any call, or use Task.Run() to make it async:
"display_name": "Harshal Zope",
var owner = JsonConvert.DeserializeObject(jsonstring);
await Task.Run(() => JsonConvert.DeserializeObject(jsonstring));
Since Microsoft recommends CPU-bound work be put in a task, what is the point where that should occur? Are small serializations/deserializations like the above CPU-bound? Are big ones? what is that threshold? How do you test for it?
If you don’t put code inside an async method in a Task.Run; what happens? If it depends on previous code; it’ll run in order; but what if it doesn’t? Does it run immediately? Besides the nano-seconds of blocking, is there any other reason to care whether everything inside an async method is awaitable?
How do you deal with synchronous libraries in asynchronous code?
RabbitMQ’s .NET client famously does not support async/await; (as an aside, have we not seen pressure to convert to async because no one is using it or because no one is using RabbitMQ in .NET?) and you’ll even get errors in some places if you try to make the code async, and they put it in their user-guide:
Symptoms of incorrect serialisation of IModel operations include, but are not limited to,
– invalid frame sequences being sent on the wire (which occurs, for example, if more than one BasicPublish operation is run simultaneously), and/or
– NotSupportedExceptions being thrown from a method in class RpcContinuationQueue complaining about “Pipelining of requests forbidden” (which occurs in situations where more than one AMQP RPC, such as ExchangeDeclare, is run simultaneously).RabbitMQ Documentation, Async.
And Stack Overflow’s advice isn’t helpful; the answer to “How do I mix async and non-async code?” is: “Don’t do that“. In another Stack Overflow post, the answer is, “Yea, you can do it with this code.“
What’s the answer? Don’t do it? Keep your entire service synchronous because the message queueing system you use doesn’t support async? Or do you convert but implement this work-around for the pieces of code that need it?
Why is it after 5 years, the adoption of async seems to be negligible? Unlike some languages, where you have no choice but to embrace async; C# as a culture still seems to treat async as a second-class citizen; and the vast majority of blogposts I’ve read on the subject go into very topical and contrived uses; without digging deeper into the real pitfalls you’ll hit when you use async in an application.
SynchronizationContext: When do I need to care about it? When do I not? Do I only care about it when it’s being used inside an object with mutable state? Do I care about it if I’m working with a pure method? What is the trigger that I can use when learning whether I need to worry about it?
It’s my experience (and partially assumption) that awaitable code that relies on other awaitable code will automatically know to wait to execute until it has the value it needs from the other awaitable code; is this true across the board? What happens when I intermix synchronous and asynchronous code?
Is it truly a problem if I have blocking code if it’s not a costly method? Will there be logic problems? Flow control issues?
Is it OK if I use a TaskCancelledException to catch client HttpClient.*Async() timeouts? Should I refactor code to use cancellation tokens, even if no user-input is ever taken in? (the service itself doesn’t accept user input; it just processes logic).
I’m not at all sure if I’m alone in having these questions and everyone else gets it; or if it’s not more widely addressed because async isn’t widely adopted. I do know that in every .NET Codebase I’ve seen since async was released, I haven’t seen anyone write new code in using async (this is a terrible metric; don’t take it as some sort of scientific assertion, it’s just what I’ve seen).