Put *Sync methods behind a flag in some future major version #1665
Description
Activity
I think synchronous methods do have their place, and shouldn't be moved behind a flag. You just shouldn't be using synchronous calls in hot code.
require()itself is synchronous.-1. Sync functions are very useful in various cases.
deasyncis definitely the worst idea ever. 1. It doesn't actually make asynchronous calls blocking 2. It's inherently broken.@cjihrig I was not talking about
require, I was talking about*Syncmethods — synchoronous variants of those methods that are async by themselves.@vkurchatkin Checked the code. Isn't it possible to create an actual lock instead of a loop?
By the way,
zlib.*Syncseems to be almost unused in comparison withfs.*Sync. For example, innode_modulesof the project I have currently opened,fs.*Syncis used in 636 files whilezlib.*Syncis not used at all.@ChALkeR I'm not sure how lock can help? To run async functions you need an event loop. It can't be the main loop, as unrelated requests would be processed as well. Something like this would work: nodejs/node-v0.x-archive#7323
+1
Only good use for sync functions is for one-line scripts, so adding a flag for it seems reasonable.
But usage of sync functions in any public modules should definitely be discouraged. Not only because of the event loop blocking, but because sync functions can't be replaced with async functions should the need for it arise.
For example, Jade uses
readFileSyncto read templates internally. What if I want to load them from a database instead? If it was an asyncreadFile, I'd just monkey-patchedrequire('fs'), but withreadFileSyncit just ain't happening.Even synchronous require is a big pain in the ass if you want to require something from a remote http server or a zip archive instead of a filesystem. We've talked about that before.
I believe sync functions should be removed completely when ES7
async/awaitsupport lands in v8. Until then we should be gradually deprecating them.@vkurchatkin nodejs/node-v0.x-archive#7323 looks like a cleaner solution to me than the current deasync module.
Once again: I do not propose to remove
*Syncjust now, I am talking about masking them in some future major version, when Promises will be treated as first-class citizens etc. And this is not about all synchronous methods, it's about those methods that have a normal async version.This might require a better way to build a sync version of an async function than the deasync module, if it is not good enough.
- addeddiscussIssues opened for discussion and feedback.Issues opened for discussion and feedback.
on May 9, 2015 I'd rather have an async-only mode that warns/throws (an option for both). Creating CLI scripts that require runtime flags is a pain in the ass.
I'm -1 on putting Sync methods behind a flag. There are plenty of valid use cases for them.
If anything, maybe we could add--trace-blockingthat prints a message whenever the event loop is blocked on i/o.@jonathanong That woldn't do anything. It would not lower
*Syncusage in modules, it wouldn't discourage newcomers from using*Syncmethods, it wouldn't motivate people to port their legacy code to async. It wouldn't solve any problem highlighted in the inital post.You could just grep for
[a-z]*Sync\(if you want to just find all usages of them, there is no need for that to be traced.@piscisaureus They introduce problems because they seem to be an easy solution at a first glance but result in huge problems later (see 2, 3). Even for use cases that look like valid there might be problems (see 4). Such flag might be useful, but this is a different question from the one that I want to discuss here. A relevant thing would be to propose some other way of discouraging newcomers (and not-so-newcomers) from (mis-)using
*Syncmethods.@piscisaureus, @vkurchatkin Please, list specific use cases for this to be constructive.
Relevant: #5 (comment)
To name some: scripting, bootstrapping, logging, using IO inside synchronous code. Sometimes blocking is fine.
async-await+ promises would be nice, but it doesn't solve all the problems.Also, point by point:
1 -
when someone with full understanding of the consequences needs them, they could be constructed from userspace
No, they couldn't
2 - let's fix the docs
3 -
When someone writes synchronous code (see 2) it limits how he or she can use async functions without rewriting most part of the logic
This point as against removing
Syncfunctions, if I understand it right4 - this looks bad, actually
5 - See 2
6 -
Promises-based code is as clean as *Sync-based code.
No, it's not.
-
when someone with full understanding of the consequences needs them, they could be constructed from userspace
No, they couldn't
By this (added some time ago, after you shared deasync considerations):
If deasync module is not good enough for this, this could be done when there is would be a good enough solution.
I meant that this whole issue would have to wait until that problem (constructing sync methods from async methods) is solved. When and if that problem would be solved, the answer would be «Yes, they could».
-
Yes, fixing the docs is part of the issue, it's mentioned at the very top. Doing only that would not solve the issue completely, but will work as a partial solution.
-
No, it's a point for discouraging being contaminated by
*Syncat the first place and against making it look like every async function should have a sync variant. No one is going to do that in all the user-contributed modules, they are generally async-only (for stuff that should be async), without sync variants. -
Yes.
zlib.*Syncis bad, broken and not actively used, it should be the first candidate for deprecating IMO. But the same should be true for all heavy memory-using*Synccode.
It's just an example for that you can not predict when sync code goes wild. Everything was made with garbage collection and async in mind. -
See 2.
-
When Promises would be first-class citizens in io.js, it would be. And do not forget about error handling, if you take error handling into account, Promises-based code even now looks much cleaner that
*Sync-based code. The latter would be a bunch of try-catches.
-
42 remaining items
@trevnorris Your example demonstrates my point on the common misperception and doesn't address the reasoning for this issue (performance degradation in high-concurrency scenarios).
Here's an example that does:
var http = require('http') var foo = require('./packageWithBuriedSync').foo // Note the ignorant all-to-common omission of Sync in the name var bar = require('./packageWithBuriedSync').bar function asyncHandler(req, res) { var i = 3 foo(sendEnd) foo(sendEnd) foo(sendEnd) function sendEnd() { if (!--i) res.end() } } function syncHandler(req, res) { bar() bar() bar() res.end() // To eliminate performance questions about closure creation function sendEndNoop() {} } http.createServer(asyncHandler).listen(8000)
// packageWithBuriedSync.js var child_process = require('child_process') var exec = child_process.exec var execSync = child_process.execSync module.exports = { foo: function (callback) { exec('ls', callback) }, bar: function () { return execSync('ls') } }
Using
ab -k -n 10000 -c 100 http://127.0.0.1:8000/exec('ls') Time taken for tests:
1 calls: 26.429 seconds 2 calls: 48.974 seconds 3 calls: 77.823 seconds 4 calls: 99.995 secondsexecSync('ls') Time taken for tests:
1 calls: 53.748 seconds 2 calls: 103.953 seconds 3 calls: 154.733 seconds 4 calls: 218.769 secondsConclusion: For IO-bound cases[1], an ignorant buried
*Syncconsistently decreased performance by 100%.@Qard Yes. See @rlidwka's comment though.
@benjamingr This is solved. The issue is the current stance is hostile to performance and an anti-pattern for new developers, thus the reasoning for proposing inclusion in core.
[1] Due to my MBA's flash drive, filesystem latency was extremely low and async performance had relative parity with
*Syncwith-c 100. An appropriate test should determine the point at which IO latency and concurrency begin to penalize performance.My preference is to close this in favor of a more pragmatic solution like #1674.
Indeed. It doesn't really stop people from using sync functions where they
shouldn't it's just a way for people that care about that sort of thing to
get warnings and maybe a stack trace of where it came from, so they can
just avoid using those bad modules.Like I said, not a fix, but it's something that can be done right now to
help optimise the performance of your code.
On May 11, 2015 1:01 PM, "Alex Kocharin" notifications@github.com wrote:Writing such a module is trivial. However, forcing all the people who
misuse Sync to install it is not as easy. :(—
Reply to this email directly or view it on GitHub
#1665 (comment).Please let's not have an argument about who does or doesn't know more node..
@Fishrock123 you're right, for what it's worth I was not making fun of @CrabDude nor was I particularly critical of his level of expertise. I was just really amused by the tone of his comments. Still am, but going to keep it at a more professional tone from now on.
@CrabDude I'm sorry if I offended you, but you got to see where I'm coming from. I never once criticized you or your ability to write serverside JavaScript. The only thing my comments were about were the tone you replied to trev (and later me).
@ChALkeR for what it's worth it was never personal, I don't recall interacting with him before.
@CrabDude You successfully demonstrated a case where using a sync alternative is slower. Those examples are plentiful. The point of my benchmark is to 1) demonstrate that async is not always faster and 2) show we shouldn't pretend we know what's best for the app developers. There are legitimate use cases for sync operations. Recall again that if you want to log information in an
uncaughtExceptionbefore the process goes down that it must be done synchronously.execSync()was added after a lot of community members reached out and requested it. Even then it took a while to make it into core because of the scale of the change that was required (see fa4eb47 and e8df267). Removing it after all that would be to spit in the community's face.I don't think you two actually disagree about anything at this point.
@trevnorris demonstrated the
Syncisn't a performance anti-pattern in non-concurrent situations.
@CrabDude demonstrated thatSyncis a performance anti-pattern in concurrent situations.Neither of you disagreed about those points. Both of you agree that it would be nice to track these performance issues in real code.
I think we should ofcus on #1674 at this point.
I think that @trevnorris is correct here:
Let's not assume we're more intelligent than the developers writing their own apps and remove functionality because it's more theoretically correct.
I initially wanted some discussion on this and I am glad that happened and that #1674 was created.
But it misses the problem behind this issue that I specially highligthed in the first (actually second) sentance of the issue. And the discussion somewhy almost missed the documentation changes that I proposed, so I opened a separate issue for that: #1684.
I guess this issue could be closed now if there are no objections.
Let's make sure we're talking about the same things...
It's stupid for us to force this type of paradigm on the user.
Stupid to remove
*Synccompletely? Yes. I differ from the OP in that I'm advocating for requiring opt-in in all scenarios where it wouldn't be "stupid" (i.e., first tick and exit).execSync() was added after a lot of community members reached out and requested it. [...] Removing it after all that would be to spit in the community's face.
Unnecessary inflammatory language aside,
*Synccalls are a liability and an asynchronous IO-bound anti-pattern, what node.js is optimized for. It's a "spit in the community's face" to put anti-patterns that undermine the runtime in core in the first place. Appeasing a vocal minority to support non-concurrent, non-asynchronous scenarios is harmful to node.js primary use case. This is precisely what I mean when I point to your unfamiliarity with application developers' dilemma. What's "stupid" is the proliferation of liabilities and anti-patterns in core without considerations for education, allowances for documentation, and meaningful support for avoidance. I think we're now all on the same page here.Let's not assume we're more intelligent than the developers writing their own apps and remove functionality because it's more theoretically correct.
First, facts are not theory. It is factually correct, demonstrated above and acknowledge by yourself that blocking calls are a major performance penalty for IO-bound tasks, which arguably account for the vast majority of node's use (numbers would be useful here). Second, the addition of
*Syncto appease a vocal minority and the pernicious liabilities it creates for the ecosystem is naive.domains, of which you are intimately familiar, perfectly illustrates another core anti-pattern that developers consistently ignorantly misused resulting in its removal despite "the scale of the change that was required." In your opinion, was that also a "spit in the community's face"?Let's just put the performance issue to bed. [...] You successfully demonstrated a case where using a sync alternative is slower. Those examples are plentiful.
Precisely. So we're in agreement that
*Syncis anti-performant in high-concurrency scenarios, which again is node's core focus, and only faster in non-concurrent scenarios.we shouldn't pretend we know what's best for the app developers.
At face value, this seems reasonable, yet in this context, it's misguided. Reductio ad absurdum:
- Who are we to say developers prefer performance over features?
- Who are we to say developers prefer non-blocking calls in a non-blocking runtime?
- Who are we to say
httpshould be in core when it's perfectly implementable in userland? - Who are we to say how modules or packages should be loaded?
You'll notice for all of the above, core does not preclude userland from implementing their own features, blocking calls, http alternatives, or module/package systems. It does however ship solutions to further enable the best non-blocking JavaScript IO runtime, which
*Synccalls after startup are deleterious to. Did you equally object thatexecSyncbelonged in userland and if so, why do you now defend its inclusion with such forceful language?There are legitimate use cases for sync operations.
Addressed at the beginning of the conversation.
#1674 and #1684 are excellent steps in the right directly, though I would prefer to see the logic that continues to support the proliferation of
*Synccalls in core to die. It was not long ago when the node community loudly advocated that a primary reason for node's success over Twisted, EventMachine, etc... was the lack of legacy (blocking) IO calls. It seems we're prone to repeat the mistakes of the past in the name of pragmatism, un-opinionatedness or "community".- addedchild_processIssues and PRs related to the child_process subsystem.Issues and PRs related to the child_process subsystem.and removedchild_processIssues and PRs related to the child_process subsystem.Issues and PRs related to the child_process subsystem.
on May 15, 2015
No, I am not writing this from a mental hospital, please read till the bottom.
The purpose of this is to discourage using
*Syncversions of the methods that could be async.I propose to put all or at least a part of
*Syncmethods behind a runtime flag in some future major version of io.js and to split documentation, moving all*Syncmethods to a separate page. This of course would be semver-major.Atm, there are
*Syncmethods defined inzlib,fs,child_process,cryptomodules and additionaly used inreplandmodulemodules.To allow usage inside io.js itself (i.e. for
require), they could be moved to «private» methods (beginning with a_sign).*Syncmethods suggest bad practices (see 2), but when someone with full understanding of the consequences needs them, they could be constructed from userspace. See https://github.com/abbr/deasync, it creates synchronous methods from async methods.If deasync module is not good enough for this, this could be done when there is would be a good enough solution.
When a newcomer begins writing something using io.js, he or she goes to the documentation looking how to do something, sees
*Syncmethods without any warnings there and almost certanly begins with using them, because that's what he or she is used to. That's easier for a newcomer than spending a few minutes reading how he or she should actually do stuff. And don't blame the newcomer, it's the presense of*Syncmethods in the documentation that suggests to him or her that it's an ok way to do things. This results in a big pile of bad code by the time when the person understands that it should be rewritten. And people don't like to rewrite code for no visible reson, leaving this code to be legacy (see 3).When someone writes synchronous code (see 2) it limits how he or she can use async functions without rewriting most part of the logic, so he or she comes complaining about that there should be a
*Syncversion of everything out there (as the presense of*Syncversions in the core suggests it). See Is there a sync version encapsulating libmagic in node.js ? mscdex/mmmagic#32 and motivation behind https://github.com/abbr/deasync.It's completely broken either way. Even for «simple one-time scripts». See zlib: memory leak with gunzipSync #1479 — a person was doing something like
files.forEach( …zlib.gzipSync(…) …), in what I suppose was a simple script. What could possibly go wrong? Memory usage has gone completely bad in his script. And even manual calls togc()do not help. Testcase:People go to the doc, see
*Syncversions (see 2), use them — then everyone are telling them that they are using io.js wrong just based on that fact: gripe: deprecating fs.exists/existsSync #1592 (comment).One can argue again that using
*Syncversions of methods is ok in scripts and that that's simplier, but:*Sync-based code. Promises-based code with accurate error handling is much cleaner that*Sync-based code with accurate error handling. Using Promisify won't be needed once Promises go to the core (see Feature Request: Every async function returns Promise #11).This will require all public modules that are using
*Syncmethods either to rewrite things using async methods or require/include something like https://github.com/abbr/deasync to be compatible.This can be done separately for various core modules/methods. For example,
zlib.*Syncare maybe the worst of them and it looks to me that they are not actively used in public modules.