-
Notifications
You must be signed in to change notification settings - Fork 30.6k
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
regression: data truncation with stream.writev in IPC channel #24992
Comments
Do you know the root cause of |
@addaleax - libuv does not throw ENOBUFS for writes AFAIK (as it already has the write buffers with it), only for reads. this is where it is thrown: Lines 106 to 107 in a1b283c
Here is the sequence of events that led to this scenario IIUC.
IMO, either
|
ok, I am able to confirm above theory - If I comment the fast path block in stream_writable Lines 504 to 519 in a3801e9
then I get the full data. So the inference and fix path remain in tact. Ability to manage chunks whose cumulative byte count exceeds |
Good analysis @gireeshpunathil! I stumbled upon some bad EAGAIN behavior recently in a userland module that I maintain (pinojs/sonic-boom#17).
Backpressure/flow control should be kicking in at this point and well-written applications should limit memory usage. Isn't it? (There are a lot of cases where it's not possible to apply backpressure correctly, such as with logging,
We are not in agreement. I think this is a problem related to how I propose:
|
@gireeshpunathil Thanks, this makes sense! I think I agree with you about our options, but just to make sure we’re in agreement: We would either
Right? @mcollina I think you’re talking about a different kind of issue; this isn’t really related to stdio specifically, or premature exits?
@mcollina The problem is that, when we provide
@mcollina I have libuv/libuv#390 on my backlog … once that is implemented, I think a clean solution would be to move stdio streams to a new, separate loop when Node exits, and to spin that loop until it exits just for stdio? |
I'm -1 to this approach, as we will keep accepting data from a misbehaving source. The source should slow down otherwise memory usage will grow without control. At some point, the process will crash for out of memory.
Then we will still error/lose data, just at a different point. It will not solve the issue.
I think it is, check the example - it's using |
agree. We just need to invoke libuv writers multiple times instead of once, slicing it appropriately. The POLLOUT watchers that libuv inserts take care of draining those at appropriate times? If the data grows indefinitely and leads to OOM at that stage, that is fine.
agree, but dispatch the rest of data in a subsequent call(s)? In other words, call native Writev only with how much data it can handle.
An approach that I was trying is here: gireeshpunathil/libuv@e6e3702
|
It is their problem. They are creating an application that is writing a huge amount of data to standard output without any backpressure controls. The root cause of the problem is that
Actually, it will end up in a OOM situation if you increase enough the amount of data that is being sent. Moreover, this will cause a huge memory usage spike that the user would not predict for. If we are not slowing the producer down, the process is going to crash anyway.
I think d85d120 just exposed an issue that has always been there. The buffering issue with pipes and
My point is that for streams, this is totally normal and expected. For |
ok, here is a version that does not use console: const n = require('net')
const KB = 1024
const MB = KB * KB
n.createServer((m) => {
var count = 0
setTimeout(() => {
m.on('data', c => count += c.length)
m.on('end', () => console.log(count))
}, 2000)
}).listen(32000, () => {
const p = n.connect(32000, () => {
const data = Buffer.alloc(KB).fill('x').toString()
for (var i = 0; i < MB; i++)
p.write(data)
p.end()
})
}) $ node noipc
|
I do not see any issue at all with that example (#24992 (comment)), and I do not think that is a bug. An application that manipulates streams without handling backpressure is going to have major issues in any case, and this is just one of many. I consider the data truncation with On a side note, I did solve this issue in pino pinojs/pino#566 by using |
@mcollina - so who / what defines a backpressure limit? is it a string whose un-tersed byte length is INT_MAX? and who controls it? If I increase From the VM perspective, it should define how much data it can handle under which abstractions, and the limitations should be documented under the corresponding APIs. Here, From an end-user perspective, inability to transport a 1GB data (that was accumulated over time due reasons outside of program's control) looks like a bug to me. And, we can actually fix it! I have come up with something that works: #25044. But given the sensitivity of the code area, would love more feedback and view points. thanks! |
Vectored writes that contain large string data (accumulated from small strings over time due to congestion in the stream) fails with ENOBUFS if the cumulative chunk size is more than INT_MAX. Under backpressure situations failure is justified in JS land with heap OOM as well as in the native land with libuv resource exhaustion etc, but the stream wrap that sits in the middle which just facilitates the transport between layers is not. Detect the large data situation, and split those at right boundaries. Carry out intermediary writes through dummy write_wrap objects to avoid multiple callbacks to the requestor. Fixes: nodejs#24992
I think that an end-user trying to append 1GB of data to a Stream synchronously has a bug in their code. We are not in agreement if Node.js should keep buffering until it crashes for OOM or it should emit a proper error if a stream do not use backpressure. I do not think this disagreement is solvable with further discussion. |
closing based on @mcollina 's feedback. I have experimented with a 'workround' in JS side as opposed to the C++ side as attempted in #25044 , that detects the diff --git a/lib/_stream_writable.js b/lib/_stream_writable.js
index 570c4c4..035cf25 100644
--- a/lib/_stream_writable.js
+++ b/lib/_stream_writable.js
@@ -28,6 +28,7 @@
module.exports = Writable;
Writable.WritableState = WritableState;
+const MAX_CHAR = 536870911; // INT_MAX / 4
const internalUtil = require('internal/util');
const Stream = require('stream');
const { Buffer } = require('buffer');
@@ -500,8 +501,17 @@ function onwriteDrain(stream, state) {
function clearBuffer(stream, state) {
state.bufferProcessing = true;
var entry = state.bufferedRequest;
+ var count = 0;
+ var fast = true;
+ while (entry) {
+ var chunk = entry.chunk;
+ count += state.objectMode ? 1 : chunk.length;
+ if (count >= MAX_CHAR) { fast = false; break;}
+ entry = entry.next;
+ }
+ entry = state.bufferedRequest;
- if (stream._writev && entry && entry.next) {
+ if (fast && stream._writev && entry && entry.next) {
// Fast case, write everything using _writev()
var l = state.bufferedRequestCount;
var buffer = new Array(l); |
My feedback is related to writing down to a stream. However, I think the console.log behavior is a bug when a pipe is used. |
Here is my take to solve this issue: #25638. |
ping @mcollina and @gireeshpunathil ... any further thoughts on what we should do here? |
My assessment is that it cannot be solved without side effects. So may be, close and re-visit if this becomes problematic for real world scenarios? |
I was working on a potential solution for the
data truncation in child standard streams upon process exit
problem, and came across this scenario:1074790400
bytes (1GB +1MB new line chars that console.log adds)actual data obtained in UNIX is much less, and is arbitrary in each run. Windows works fine.
The behavior is well understood in the presence of
process.exit
but in the absence of it, the process is supposed to drain all the data while there are active requests in the event loop. That is not happening here.Tracing back, I landed at d85d120 which introduced vectored writes in pipe streams. With this in place, when the channel experiences congestion, it throws UV_ENOBUFS which is passed to the calling code unhandled:
Further, commit 1d6b729 inadvertently caused this error from not being visible.
Subsequently there are lot of changes in stream_base.cc and stream_wrap.cc around
DoTryWrite
andWritev
which makes it difficult to pin-point any particular point where this can be rectified.Looking at
Writev
implementation, it does not handle when the data size is more thanINT_MAX
. In my opinion this should be fixed, as vectored write is an internal mechanism that should hide itself from the JS API. Also, looks like it does not leverage libuv'swrite-when-you-can
capability (uv__write
)?/cc @nodejs/streams @nodejs/child_process
Happy to do any further assistance / testing.
The text was updated successfully, but these errors were encountered: