> Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.
And in that spirit, I very much would like to underestimate myself, and write things that are much simpler than I am capable of handling.
> There’s a small chance that the current time will have advanced by the time the subtraction is done, resulting in a negative value passed to wait() and an IllegalArgumentException getting thrown.
My general rule when there's a potential for race condition bugs like this is you should multiply your paranoia for looking for bugs by at least 10, and very heavily consider alternatives that aren't subject to race conditions because it's impractical to get everyone who might review, extend or maintain the code to be sufficiently paranoid.
For race conditions, it feels like you really have to experience the wide potential for bugs yourself during development to understand how unpredictable, unintuitive, and catastrophic the unhappy code paths can be.
When I'm editing or writing code that needs to be really robust, I'm usually thinking about the state space and control flow diagram in my head. If there's too many paths that aren't likely to be explored during regular manual testing, test suite runs, and normal user usage, that's a sign to do something like push back on if the requirements need to be this complex, or if there's some code improvements we can make like refactoring or better abstractions. Otherwise there's likely to be subtle bugs hiding somewhere. Potential race conditions in particular usually explode the state space.
Lately I've been thinking about the airplane design analogy for writing software. Supposedly (possibly apocryphally) Bill Gates said that measuring software development progress by lines of code written was like measuring aircraft design progress by weight.
I think the analogy is a valuable one.
On an airplane, you might look for places where the airframe is stronger than it needs to be. If you can save material there, it'll be not just cheaper but lighter and more efficient. If the weight savings improves the flight characteristics of the aircraft, or if allows for the addition of safety features elsewhere, it might improve safety as well.
This is a really hard conversation to have in software, because when people think they can make something better with a lot of extra code, a lot of times they'll just do it, even if they know that other people think it's unnecessary. They put in the extra time on their own responsibility, thinking that the only downside is the initial work to write the code, and once it's done, it becomes an asset. It sucks to tell them that I'd rather not have it, that I'd rather have the easy version they could have done in an hour. I give in way more often than I'd like to.
> This is a really hard conversation to have in software, because when people think they can make something better with a lot of extra code, a lot of times they'll just do it, even if they know that other people think it's unnecessary. They put in the extra time on their own responsibility, thinking that the only downside is the initial work to write the code, and once it's done, it becomes an asset. It sucks to tell them that I'd rather not have it, that I'd rather have the easy version they could have done in an hour. I give in way more often than I'd like to.
I feel like that's a perfect case for making software more modular? Whether it's runtime plugins or compile-time options (with stable APIs and keeping things separate), it's possible to let people who want a feature have it without affecting anyone else.
Modularity isn’t free. It’s useful for many things, but assuming it’s inherently valuable is how you end up with enterprise bloat.
Programming languages are inherently flexible, however when you have 2 features and a configuration option to select between them you now have 3 features.
> Programming languages are inherently flexible, however when you have 2 features and a configuration option to select between them you now have 3 features.
Which is a good argument for separating things into plugins: If you have 1 stable API/ABI and 100 plugins, you have 1 feature.
If you’re writing the software and the plugins you have 101 features and likely a bunch of complicated integrations between plugins or when people change them.
It’s only really useful if you’re getting 3rd parties to support plugins written by them.
Now you need to pay someone to implement a solution for automatically packaging and distributing your plugins. You have extra complexity on every project to manage pulling dependencies. Someone needs to maintain all those packages and the infrastructure to serve them.
I'm doing this right now for my company. Starting from zero automation it's taken probably half my time these last few months. Every project needs individual care. Some projects require major refactors to use the new libraries instead of their own implementation. I have to secure our releases so we don't leak credentials, I have to secure the GitLab server, we have a half dozen new accounts and keys to keep track of forever.
That kind of modularity really isn't free at all, in any sense.
Conversely, it's far better to have a slightly more verbose section of code that is immediately readable than a super slick one-liner that like two people can parse.
Maintainability is, in my opinion, just as important as other considerations when writing code. And often the more verbose approach is composed of simpler steps, meaning bugs can be easier to spot during code reviews
I like the author's original example the best, but:
while ((message = map.get(key)) == null) {
long now = System.currentTimeMillis();
if (now >= timeoutTime) {
break;
}
wait(timeoutTime - now);
}
If I had my way, then all "sleep" APIs would take a deadline time point, not a time duration. Also have the API do nothing if the deadline is passed. This way, getting the subtraction right need only happen in the lowest shared part of the library. Then it's a lot like the original example:
while ((message = map.get(key)) == null) {
waitUntil(timeoutTime);
}
edit: Oops, my last example has a pretty bad bug. The author's point stands, and I need coffee.
You could have waitUntil return true or false to indicate whether the wait occurred or not, but then that loses the opportunity to convey whether the thread was awakened due to an operation on the map.
do {
message = map.get(key);
} while (message == null && waitUntil(timeoutTime));
Huge fan of minimizing Kolmogorov complexity [1]. There's a balance, but typically if the same behavior can be described in less code, then the simplicity will yield dividends.
There's an article in IEEE software engineering proceedings around the turn of the millennium that said that defect count was closely associated with any complexity measure you wanted - - including lines of code.
I once said "nearly half of the problems are logic errors, nearly half are misused APIs" and got the retort "and another half are from concurrency".
Of course that as up to nearly 150%, which is probably most people's actual bug count...
What bothers me is that some programmers think that writing the code more dense is already better. But I would argue it's not the characters / less lines of code which creates the complexity, but how many logical concepts (?) you utilize to solve the problem.
Using a smaller set of different concepts also helps reducing the cognitive load, even if it leads to more verbosity ("less clever code").
This is close to what I would have written. It is almost never about actually the line count. Not even SLOC. For example different languages lend themselves to breaking lines to a different degree. In Scheme I almost always write (define name \nl (lambda (arguments) \nl ...)) Did I now waste a line? Of course not. It is still the same number of concepts and basically tokens involved as would be in Python, when I write "def name(arguments):" but Python doesn't lend itself that well to line breaking, because of its (annoying) whitespace sensitivity. Neither does this make code any more or less readable nor does it increase the chance for bugs. Same goes for many other constructs in both languages. Take a "(cond ...)" for example. I will break some lines there, because the language makes it easy, by having everything delimited with parents, while in Python I will have to type additional visual clutter to do the same.
Everything has tradeoffs, but there's value in reducing both line and character counts as well.
For example, nobody ever uses anything other than ijk for loop indices unless the index is particularly meaningful or they've written a deeply nested abomination. Why? It's not just laziness in typing; it gives more relative room for characters that matter. Longer names and patterns are acceptable if you can't make your point clearly enough with few characters, but length isn't the goal; communication is.
It's important to limit lines of code too (and their widths) because if an idea doesn't fit comfortably on your screen then you won't be able to leverage the pattern-recognition parts of your brain to figure out what's going on.
From a different perspective, you know that feeling you get when somebody dumps a 1000-line PR on you (or an excessively long HN comment...)? It's hard to digest because you can't grok the whole thing at once and have to switch to carefully analyzing each component just to even have the context to then give the thing a proper review. If that same PR could be wired together with a few high-level concepts (less code, but more involved baseline knowledge required to understand it), it would be instantly understandable to somebody with the same background.
I'm looking at this feeling anxiety about whether or not the implementation of `map.get` is threadsafe with memories of deadlocks in unsafe hashset implementations whilst accessing values during resizing.
When what they really wanted here for bug free multithreading is some kind of promise or async await abstraction.
It turns out that you just need to make reasoned assessments about how the system[*] you're building on works; what you need to make it do today, what you're likely to need to make it do tomorrow; how your work impacts other parts of the system today and tomorrow; what resource pressures your system is facing; etc
As written with its language of absolutes, the article gives horrible advice. If you were to follow it at every opportunity, you would write software that becomes more and more terribly brittle, terribly inefficient, terribly illegible, terrible incapable over time.
Our craft is one of engineering. It takes years of study and practice to learn to do it well. You never stop learning to take more into consideration and never overcome having to make hard, arguable decisions in light of both known and unknown unknowns. It just doesn't reduce to adages like this and you start building a wall in front of your own professional growth when start assuming it does.
--
[*] where system is inclusive of your skills and fluency, your development tools, your deployment environment, your team and their skills and preferences, your stakeholders and their requirements, your users, your budget of time and capital and mental energy, etc
> I wrote some code to get a message out of a hash table. The message was going to be put there by another thread. There was a small chance of a race where it wouldn’t be there yet when I initially looked for it. The code looked something like this:
> The wait() call blocks the thread, waiting for the notifyAll() from the thread that puts the message into the map. The 1000 means one second. The timeout was going to be on the order of five seconds.
> The above code is simple and correct
… it looks neither simple nor correct.
If this is multithreaded code, "small chance of a race" is a smell, and "notifyAll" is probably a smell. The multithreading primitive that I'd want to see here is a condition, so named because it permits a thread to wait for a particular condition. A condition wakes the "notified" thread with a mutex already locked, so that the condition ("is there a message in the table?") can be checked without interference from other threads. If there is a message, you can remove it, process it, etc. without races, and if there isn't, you can atomically release the lock and go back to sleep (which is again a thing the condition provides; it is not two separate calls).
The other surprising thing is that two code reviewers also evidently missed the opportunity to re-write it properly using thread synchronization primitives. Rather they argued about what to pass to wait(), except you shouldn't be using wait() or sleep() here at all!
They should be miles away from production code, mission critical or otherwise. Unless they just love eating hard-to-reproduce bugs for breakfast. Do they teach mutexes and semaphores in university anymore?
That what was in my mind, the second I’ve seen the code. I would‘ be pushed so hard to make the while go away. In high level code, it’s simple wrong. Since his code is Java it looks even more suspicious because of the hashmap.
Throw in a mutex and a condition variable. Many languages and systems (including C++) with locks allow you to set a timeout (either a specific time to wake or a delay and then wake). Use a notify in the write thread to wake the reader.
EDIT: I had C++ on the brain, the code is in Java and they're already using notifyAll. The wait should just be to the timeout limit and not shorter. If the data is ready in under a second, the notifyAll will wake the thread and it can keep running and there's no risk of waiting an extra second (approx) past the actual deadline as the current code would do.
Honestly the fact that a thread is even waiting for a map value to be filled in is a bit of a red flag. 99% of multithreaded code should be using channels or parallel for/map.
If you have to have a thread wait for a value from another then use a condition variable like he said.
99.999% of the time if you've hardcoded a delay, you should have done anything else. The correct tool here is cross-thread synchronization. You can do this in many ways, but for a simple situation like this, I'd use a mutex or a semaphore or something.
In my opinion as an embedded programmer, the only time it's appropriate to hardcode a delay this way is if you're actually waiting on a physical process to happen. For instance, when I bring up a power rail in my widget, I have a delay of a few hundred milliseconds for capacitors to charge, power to stabilize, and chips to boot. Waiting five seconds for another thread to push data is not appropriate.
We have entire ecosystems of tools to communicate between threads. Sleeping a thread is not one of those, and you typically only see that if the author doesn't understand the tools properly. It's a very beginner mistake.
The code the author gave is Java, so the hash map is most likely a ConcurrentHashMap, and it's safe to access and modify concurrently. The author's code is simplistic and inefficient, but it won't deadlock, it won't corrupt the hash map, it won't go more than a second past the timeout, and it won't fail to retrieve the data if the other thread sets it in a reasonable amount of time.
To me, the smell is not that the author is using the wrong multithreading primitives but that they're using multithreading primitives at all. The first step if they want a better solution (and if the context allows them to rewrite the code in question) is to pick one of the many pre-packaged concurrency abstractions that will be safer and more optimized than 99.99% of programmers could write for themselves. Unless they have an extremely unusual problem, which they almost certainly don't, they don't need to be messing with mutexes and conditions at all.
Honestly, judging from the style of the concurrency, I would guess that they're working with 15+ year old legacy code, and it's very possible that there's a bizarre reason they can't fix it.
The wait method can only be invoked when the object is locked (e.g. within a synchronized block). The method throws an IllegalMonitorStateException otherwise. This implies that the map access is protected by that lock. Entering the wait releases the lock, and leaving the wait reacquires the lock. Those lock operations also imply a happens-before ordering, meaning changes made to the map by another thread during the wait are guaranteed to become visible to the current thread.
The code is perfectly correct, and straightforward if you’re familiar with Java concurrency basics. Wait and notify were designed to be used in that way to wait for a condition. This is (or was) a common Java synchronization idiom:
Regarding the “small chance of a race”, the code’s whole purpose is to remove that race by synchronizing properly, which it does. The author presumably didn’t show the surrounding lock because it is implied by the wait, and because they were assuming their audience to be familiar with the idiom.
Sadly, this has become a fairly outdated view of how to do software. Tbh, I'm not sure if it ever really caught on, but there was sometime where it was gaining momentum. That's long gone though.
People will ask and expect every single part of your code to take in as much complexity as possible because "well, it makes sense". Take something as simple as a timeout value for a task.
- We need to add a timeout because list reasonable reasons to have timeouts
- Hey, so the timeout should only apply for external IO. Some tasks are just CPU intensive and that shouldn't cause the timeout to kick in. Like sure tasks usually take 10 minutes, but if it's taking more because of CPU, then we should let it.
- Hey, so we had an infinite loop bug in a task. So it was consuming a lot of CPU, but shouldn't have waited an hour to timeout. If there is no stdout, then its safe to assume it's stuck for other reasons.
- So this task runs ffmpeg, and there is no stdout, but it's actually running and should be fine. so check if ffmpeg process is running and handle it differently.
- Hey, so the timeout should be different for this one endpoint X. We know that it's slow, so we should increase the timeout only for that endpoint and not count it in the overall timeout.
- So that endpoint X has a fairly high timeout today. However, they had an outage yesterday and all our calls took hours to eventually timeout. We should have the timeout be adaptive depending on the outcome of previous attempts per endpoint.
- So the adaptive timeout logic kinda tripped over itself here. See, service Y is usually fast, so it's timeout is low. But last week they had an issue, and timeout was taking 20 minutes for all task. It looks like the adaptive-timeout-system took the timeout up to 20 minutes for it. But it shouldn't have. For service Y, it should have have only gone up to 2 minutes max. We need to add limits to the adaptive-timeout-system
- Can we determine these adaptive-timeout-system limits automatically? It's a pain in the ass to need to figure out what is the right timeout value for each service we call.
- So what if the value of the timeout came from an LLM. So we give the LLM the information about the current task, and what we know about the the previous calls, then the LLM suggests a value for the timeout.
Using notifyAll and wait in Java is tricky to do correctly, you'd have a much easier chance using concurrency primitives that wrap these functions, like CountDownLatch[1] for one offs, or Condition[2].
System.currentTimeMillis is also not guaranteed to be monotonic, and System.nanoTime should be used instead. This would of course be obviated if a better concurrency primitive were to be used.
All the smartest people are humble and realise the limits of their capability. as a result they automatically make decisions that reduce complexity. unfortunately software development is full of people who overestimate their capability with obvious results
What's confusing me is that both reviewers and several commenters here are suggesting code that is simply incorrect.
If you wait for the time between now and the timeout, you mandate that if the value is not in the hashmap immediately, the program will sleep for ~5 seconds no matter what. "the key was very likely to be there within the first second" is the entire rebuttal to their suggestion. You don't need anything else, why would you sleep for 5 seconds if you can only sleep for 1? Do you not care about runtime performance? Do you have no respect for the user who has to wait for the output?
Why wake up if your condition is not yet met? The wait timeout is the time after it should continue if it was not notified. It is meant to be waken up as soon as possible by a notify. If the value is available almost immediately it will wake up almost immediately whether it is wait(1000) or wait(5000).
63 comments
[ 3.2 ms ] story [ 79.2 ms ] thread> Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.
And in that spirit, I very much would like to underestimate myself, and write things that are much simpler than I am capable of handling.
My general rule when there's a potential for race condition bugs like this is you should multiply your paranoia for looking for bugs by at least 10, and very heavily consider alternatives that aren't subject to race conditions because it's impractical to get everyone who might review, extend or maintain the code to be sufficiently paranoid.
For race conditions, it feels like you really have to experience the wide potential for bugs yourself during development to understand how unpredictable, unintuitive, and catastrophic the unhappy code paths can be.
When I'm editing or writing code that needs to be really robust, I'm usually thinking about the state space and control flow diagram in my head. If there's too many paths that aren't likely to be explored during regular manual testing, test suite runs, and normal user usage, that's a sign to do something like push back on if the requirements need to be this complex, or if there's some code improvements we can make like refactoring or better abstractions. Otherwise there's likely to be subtle bugs hiding somewhere. Potential race conditions in particular usually explode the state space.
Or perhaps: Bugs happen.
Technically, the reviewer didn't introduce any new lines
I think the analogy is a valuable one.
On an airplane, you might look for places where the airframe is stronger than it needs to be. If you can save material there, it'll be not just cheaper but lighter and more efficient. If the weight savings improves the flight characteristics of the aircraft, or if allows for the addition of safety features elsewhere, it might improve safety as well.
This is a really hard conversation to have in software, because when people think they can make something better with a lot of extra code, a lot of times they'll just do it, even if they know that other people think it's unnecessary. They put in the extra time on their own responsibility, thinking that the only downside is the initial work to write the code, and once it's done, it becomes an asset. It sucks to tell them that I'd rather not have it, that I'd rather have the easy version they could have done in an hour. I give in way more often than I'd like to.
I like to say two things about software:
Code is a liability
The best code is no code
* "every line is a potential bug"
I feel like that's a perfect case for making software more modular? Whether it's runtime plugins or compile-time options (with stable APIs and keeping things separate), it's possible to let people who want a feature have it without affecting anyone else.
Programming languages are inherently flexible, however when you have 2 features and a configuration option to select between them you now have 3 features.
Which is a good argument for separating things into plugins: If you have 1 stable API/ABI and 100 plugins, you have 1 feature.
It’s only really useful if you’re getting 3rd parties to support plugins written by them.
I'm doing this right now for my company. Starting from zero automation it's taken probably half my time these last few months. Every project needs individual care. Some projects require major refactors to use the new libraries instead of their own implementation. I have to secure our releases so we don't leak credentials, I have to secure the GitLab server, we have a half dozen new accounts and keys to keep track of forever.
That kind of modularity really isn't free at all, in any sense.
Maintainability is, in my opinion, just as important as other considerations when writing code. And often the more verbose approach is composed of simpler steps, meaning bugs can be easier to spot during code reviews
1. One reasonable line of code
2. Several lines of plain code
3. A function call, moving the implementation elsewhere
4. A gnarly one-liner
(The tough part about this is drawing the line between 1 & 4.)
You could have waitUntil return true or false to indicate whether the wait occurred or not, but then that loses the opportunity to convey whether the thread was awakened due to an operation on the map.
Author's original example still wins.[1] https://en.wikipedia.org/wiki/Kolmogorov_complexity
I once said "nearly half of the problems are logic errors, nearly half are misused APIs" and got the retort "and another half are from concurrency".
Of course that as up to nearly 150%, which is probably most people's actual bug count...
Using a smaller set of different concepts also helps reducing the cognitive load, even if it leads to more verbosity ("less clever code").
For example, nobody ever uses anything other than ijk for loop indices unless the index is particularly meaningful or they've written a deeply nested abomination. Why? It's not just laziness in typing; it gives more relative room for characters that matter. Longer names and patterns are acceptable if you can't make your point clearly enough with few characters, but length isn't the goal; communication is.
It's important to limit lines of code too (and their widths) because if an idea doesn't fit comfortably on your screen then you won't be able to leverage the pattern-recognition parts of your brain to figure out what's going on.
From a different perspective, you know that feeling you get when somebody dumps a 1000-line PR on you (or an excessively long HN comment...)? It's hard to digest because you can't grok the whole thing at once and have to switch to carefully analyzing each component just to even have the context to then give the thing a proper review. If that same PR could be wired together with a few high-level concepts (less code, but more involved baseline knowledge required to understand it), it would be instantly understandable to somebody with the same background.
Magic number (1000) unrelated to the timeout constant must have triggered strong emotions.
I think that the moral of the story is a bit different.
When what they really wanted here for bug free multithreading is some kind of promise or async await abstraction.
https://flatliner.herokuapp.com/
https://github.com/pajecawav/ghloc-web#badges
It turns out that you just need to make reasoned assessments about how the system[*] you're building on works; what you need to make it do today, what you're likely to need to make it do tomorrow; how your work impacts other parts of the system today and tomorrow; what resource pressures your system is facing; etc
As written with its language of absolutes, the article gives horrible advice. If you were to follow it at every opportunity, you would write software that becomes more and more terribly brittle, terribly inefficient, terribly illegible, terrible incapable over time.
Our craft is one of engineering. It takes years of study and practice to learn to do it well. You never stop learning to take more into consideration and never overcome having to make hard, arguable decisions in light of both known and unknown unknowns. It just doesn't reduce to adages like this and you start building a wall in front of your own professional growth when start assuming it does.
--
[*] where system is inclusive of your skills and fluency, your development tools, your deployment environment, your team and their skills and preferences, your stakeholders and their requirements, your users, your budget of time and capital and mental energy, etc
> The above code is simple and correct
… it looks neither simple nor correct.
If this is multithreaded code, "small chance of a race" is a smell, and "notifyAll" is probably a smell. The multithreading primitive that I'd want to see here is a condition, so named because it permits a thread to wait for a particular condition. A condition wakes the "notified" thread with a mutex already locked, so that the condition ("is there a message in the table?") can be checked without interference from other threads. If there is a message, you can remove it, process it, etc. without races, and if there isn't, you can atomically release the lock and go back to sleep (which is again a thing the condition provides; it is not two separate calls).
That being said, these people should all be miles away from mission critical code.
It’s basically a ‚work around’ bugfix.
https://en.cppreference.com/w/cpp/thread/condition_variable - C++11 so not available in this form to the blog writer, but an equivalent was.
EDIT: I had C++ on the brain, the code is in Java and they're already using notifyAll. The wait should just be to the timeout limit and not shorter. If the data is ready in under a second, the notifyAll will wake the thread and it can keep running and there's no risk of waiting an extra second (approx) past the actual deadline as the current code would do.
If you have to have a thread wait for a value from another then use a condition variable like he said.
https://docs.oracle.com/javase/8/docs/api/java/util/concurre...
In my opinion as an embedded programmer, the only time it's appropriate to hardcode a delay this way is if you're actually waiting on a physical process to happen. For instance, when I bring up a power rail in my widget, I have a delay of a few hundred milliseconds for capacitors to charge, power to stabilize, and chips to boot. Waiting five seconds for another thread to push data is not appropriate.
We have entire ecosystems of tools to communicate between threads. Sleeping a thread is not one of those, and you typically only see that if the author doesn't understand the tools properly. It's a very beginner mistake.
To me, the smell is not that the author is using the wrong multithreading primitives but that they're using multithreading primitives at all. The first step if they want a better solution (and if the context allows them to rewrite the code in question) is to pick one of the many pre-packaged concurrency abstractions that will be safer and more optimized than 99.99% of programmers could write for themselves. Unless they have an extremely unusual problem, which they almost certainly don't, they don't need to be messing with mutexes and conditions at all.
Honestly, judging from the style of the concurrency, I would guess that they're working with 15+ year old legacy code, and it's very possible that there's a bizarre reason they can't fix it.
The code is perfectly correct, and straightforward if you’re familiar with Java concurrency basics. Wait and notify were designed to be used in that way to wait for a condition. This is (or was) a common Java synchronization idiom:
https://wiki.sei.cmu.edu/confluence/display/java/THI03-J.+Al...
https://docs.oracle.com/javase/tutorial/essential/concurrenc...
Regarding the “small chance of a race”, the code’s whole purpose is to remove that race by synchronizing properly, which it does. The author presumably didn’t show the surrounding lock because it is implied by the wait, and because they were assuming their audience to be familiar with the idiom.
People will ask and expect every single part of your code to take in as much complexity as possible because "well, it makes sense". Take something as simple as a timeout value for a task.
- We need to add a timeout because list reasonable reasons to have timeouts
- Hey, so the timeout should only apply for external IO. Some tasks are just CPU intensive and that shouldn't cause the timeout to kick in. Like sure tasks usually take 10 minutes, but if it's taking more because of CPU, then we should let it.
- Hey, so we had an infinite loop bug in a task. So it was consuming a lot of CPU, but shouldn't have waited an hour to timeout. If there is no stdout, then its safe to assume it's stuck for other reasons.
- So this task runs ffmpeg, and there is no stdout, but it's actually running and should be fine. so check if ffmpeg process is running and handle it differently.
- Hey, so the timeout should be different for this one endpoint X. We know that it's slow, so we should increase the timeout only for that endpoint and not count it in the overall timeout.
- So that endpoint X has a fairly high timeout today. However, they had an outage yesterday and all our calls took hours to eventually timeout. We should have the timeout be adaptive depending on the outcome of previous attempts per endpoint.
- So the adaptive timeout logic kinda tripped over itself here. See, service Y is usually fast, so it's timeout is low. But last week they had an issue, and timeout was taking 20 minutes for all task. It looks like the adaptive-timeout-system took the timeout up to 20 minutes for it. But it shouldn't have. For service Y, it should have have only gone up to 2 minutes max. We need to add limits to the adaptive-timeout-system
- Can we determine these adaptive-timeout-system limits automatically? It's a pain in the ass to need to figure out what is the right timeout value for each service we call.
- So what if the value of the timeout came from an LLM. So we give the LLM the information about the current task, and what we know about the the previous calls, then the LLM suggests a value for the timeout.
That's the wrong attitude. Every line of code has the potential for multiple bugs. Don't be afraid to think big.
Unfortunately not:
Using notifyAll and wait in Java is tricky to do correctly, you'd have a much easier chance using concurrency primitives that wrap these functions, like CountDownLatch[1] for one offs, or Condition[2].
System.currentTimeMillis is also not guaranteed to be monotonic, and System.nanoTime should be used instead. This would of course be obviated if a better concurrency primitive were to be used.
[1] https://docs.oracle.com/en/java/javase/21/docs/api/java.base...
[2] https://docs.oracle.com/en/java/javase/21/docs/api/java.base...
Every line is potentially /many/ bugs.
If you wait for the time between now and the timeout, you mandate that if the value is not in the hashmap immediately, the program will sleep for ~5 seconds no matter what. "the key was very likely to be there within the first second" is the entire rebuttal to their suggestion. You don't need anything else, why would you sleep for 5 seconds if you can only sleep for 1? Do you not care about runtime performance? Do you have no respect for the user who has to wait for the output?