-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Standardise Asynchronous Logging and Message Encoding on Windows #22
Comments
See jestjs/jest#9324 (comment) for the buggy jest that occurs when asynchronous logging is occurring. This must be because In doing so, logs must be output after the tests finishes and we get a exit code of 1. Fixing that problem is more than fixing up the It may be a good idea to provide a different library to just fix this globally, not just for the |
Note that |
Ok I've found out some more interesting information:
This means I can see that in PK this is currently being used:
These should be refactored out. Interesting the usage of that package. I'm a bit confused right now what is synchronous and what isn't synchronous. I searched over the nodejs codebase and I can see that it already calls
This is referenced from But the comments in the issue seem to indicate to me that piping is asynchronous, but this is not the case for windows because windows doesn't support asynchronous piping. In that case, it's possible to result in a memory exhaustion when logging to much to a pipe. I wonder then...
|
I did a few tests on windows and linux... note that It is in fact accurate when you run a script:
And on windows you do
And if you don't redirect stderr, then it stays as |
Further testing... process.stdout.write(
'First\n',
(e) => {
process.stdout.write('Last\n');
}
);
process.stdout.write('Second\n'); The above always prints:
Regardless of whether the stream is blocking or not. This is important to note, because that means (which is obvious in hindsight) that setting blocking or non-blocking on the stream handler is independent of the callback execution order. One might think that if it was synchronous..., then the callback should execute before the last But this does mean that the function call itself might be asynchronous even if the stream itself is blocking. |
The most recent changes (https://gitlab.com/MatrixAI/open-source/typescript-demo-lib/-/jobs/2795324826) to making the stream blocking while putting it into This is all quite confusing. When using |
Also it's important to note that So I guess you cannot control it if you are piping it. |
Ok so it appears that we can gain some determinism by always writing with promisified version of const stdoutWrite = async (message) => {
return new Promise((resolve, reject) => {
const flushed = process.stdout.write(message, (e) => {
if (e != null) return reject(e);
return resolve(flushed);
});
});
}; However this is a different problem altogether about whether it is buffered/blocking or not. A promisified version of However this changes the API of the logger, you now do I'm not really sure if this fixes the problem though... I can only imagine out of order messages due to interleaved writes, and this can still happen with |
Ok some more information. The This ends up calling:
This function is documented in http://docs.libuv.org/en/v1.x/stream.html#c.uv_stream_set_blocking. Note:
So at the libuv layer, this implies it is non-blocking by default. This explains why the node code ends up setting it to be blocking mode in It does say that on Windows, it is only supported for And so this comment:
Explains why.
Let's settle this a bit
So this explains:
Now how to make windows blocking...? You can't, libuv doesn't support it. So the fact that nodejs makes TTYs blocking for Linux and MacOS is only for convenience. So in order to standardise behaviour here, one would need to instead to promisify the callback. There are technically 2 ways to do this as documented in https://nodejs.org/api/stream.html#writablewritechunk-encoding-callback
Relying on the drain event allows one to make use of Node's own internal in-memory buffer for the stream. If however instead we used the callback, that would ignore the internal in-memory buffer, and instead wait for the in-memory buffer to flush. I guess that would be less efficient since you're basically checking for the drain event even if your internal in-memory buffer isn't exhausted. Now when flushed, it's still going to go to OS in-kernel buffer if the socket is set to be non-blocking (which is the case for Windows). |
This is very confusing because the settings have changed over and over. Ok I can see that in This was done 9 years ago in nodejs/node@20176a9, and it seems kind of like an oversight. Now it's doing something different from the rest of the systems, but has been kept for backwards compatibility. |
This would meany any kind of piping on windows is going to be quite slow for NodeJS apps when writing to |
Ok based on all this information, it seems what we need to do is promisify The promise will resolve when the data is flushed to in-kernel buffer (if any). The logger library won't control whether the streams are blocking or non-blocking. That will be left to the application. But the application can maintain some consistency by always awaiting the logging. This would mean application-wise we would need to change to using Users can still not do that, and just let Error based logging is best done with an This should have minimal performance differences on linux, macos for TTYs because they are already blocking, and on Windows, it aligns the behaviour. However if can have performance drops if the output is to a pipe/file. |
As for fixing the bug in jest, this seems like a case where Jest shouldn't be exiting with non-0 code just because of additional logging. There's also no point in setting blocking... especially because only pipes support blocking, and they are already blocking by default. |
Ok at the very least, we have to return a promise that can be optionally awaited for. As for |
It turns out that when running jest tests, the stdio are not TTY streams, but they are pipe streams instead. See the jobs:
In all 3 cases, checking The I also tested this locally, and they are still pipes. In that case any deviance in behaviour is not due to TTY differences in blocking/non-blocking, but that Windows is actually blocking on pipes, while Linux and MacOS have non-blocking for their pipes. Therefore again setting Ok so alot of the above about TTYs don't apply to this problem, because the stdio streams are just pipes, and they are actually opposite. They are non-blocking in linux/macos, while being blocking on windows. |
Specification
Using
process.stderr
andconsole.error
behaves differently when on Windows compared Linux and MacOS.By applying this fix to our stream handlers:
It ends up making Windows behave the same as Linux and MacOS.
The same needs to be applied to
stderr
as well.Note that we leave
ConsoleErrHandler
the same as usual because it's not necessarily going to nodejs.This means we should probably create
StandardOutHandler
andStandardErrHandler
, which is a breaking change for any usage ofStreamHandler
. This requires a rename in downstream projects... primarily tests in DB, EFS and PK.Additionally Powershell currently outputs in UTF-16LE. This is a bit weird when piping the output of the stderr/stdout to a file. We can write in code:
Just to be really sure (but it is a global change note). One idea is to only set it when we outputting a log, to avoid global changes, we can tack on a
utf-8
setting. Or we can respect whatever has been set in the application.That being said, it won't work for powershell, only CMD. So powershell requires an update to the latest powershell core for it work properly.
As for CRLF vs LF, always use LF in our logs. Both powershell and cmd understand and render this properly.
Additional context
Tasks
The text was updated successfully, but these errors were encountered: