Repository: pyodide/pyodide

Issue:
New Pyodide fatal error in scipy tests: Error: EAGAIN: resource temporarily unavailable, write
This started to happen two days ago in https://github.com/lesteve/scipy-tests-pyodide, here is [a build log](https://github.com/lesteve/scipy-tests-pyodide/actions/runs/5946896593/job/16128148017).

The stack trace looks like this:
```
Error: EAGAIN: resource temporarily unavailable, write
    at Object.writeSync (node:fs:936:3)
    at ue.write (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:6566:23)
    at Object.write (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:6301:28)
    at Object.write (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:12457:46)
    at doWritev (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:19506:23)
    at _fd_write (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:19589:19)
    at write (wasm://wasm/025b4bda:wasm-function[9088]:0x45849f)
    at _Py_write (wasm://wasm/025b4bda:wasm-function[4144]:0x2d9eec)
    at _io_FileIO_write (wasm://wasm/025b4bda:wasm-function[6443]:0x39de9f)
    at _PyCFunctionWithKeywords_TrampolineCall (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:6855:33) {
  errno: -11,
  syscall: 'write',
  code: 'EAGAIN',
  pyodide_fatal_error: true
}
```

For some reason, it seems to happen right at the end of `scipy.special.tests` when pytest is printing its summary. In my experience, the timing of stdout vs stderr can not be fully trusted so maybe it happens in a test towards the end of scipy.special.tests. I'll be able to look into it more next week.

My wild guess is that this could be related to #4035?
New Pyodide fatal error in scipy tests: Error: EAGAIN: resource temporarily unavailable, write
This started to happen two days ago in https://github.com/lesteve/scipy-tests-pyodide, here is [a build log](https://github.com/lesteve/scipy-tests-pyodide/actions/runs/5946896593/job/16128148017).

The stack trace looks like this:
```
Error: EAGAIN: resource temporarily unavailable, write
    at Object.writeSync (node:fs:936:3)
    at ue.write (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:6566:23)
    at Object.write (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:6301:28)
    at Object.write (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:12457:46)
    at doWritev (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:19506:23)
    at _fd_write (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:19589:19)
    at write (wasm://wasm/025b4bda:wasm-function[9088]:0x45849f)
    at _Py_write (wasm://wasm/025b4bda:wasm-function[4144]:0x2d9eec)
    at _io_FileIO_write (wasm://wasm/025b4bda:wasm-function[6443]:0x39de9f)
    at _PyCFunctionWithKeywords_TrampolineCall (/home/runner/work/scipy-tests-pyodide/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:6855:33) {
  errno: -11,
  syscall: 'write',
  code: 'EAGAIN',
  pyodide_fatal_error: true
}
```

For some reason, it seems to happen right at the end of `scipy.special.tests` when pytest is printing its summary. In my experience, the timing of stdout vs stderr can not be fully trusted so maybe it happens in a test towards the end of scipy.special.tests. I'll be able to look into it more next week.

My wild guess is that this could be related to #4035?


Discussion:
Sure sounds related, thanks for the report!
Seems like we already solved a similar problem with reads from stdin with `readHelper` here:
https://github.com/pyodide/pyodide/blob/main/src/js/streams.ts#L93-L110
I think we just need the same logic for `writeHelper`.

Sure sounds related, thanks for the report!
Seems like we already solved a similar problem with reads from stdin with `readHelper` here:
https://github.com/pyodide/pyodide/blob/main/src/js/streams.ts#L93-L110
I think we just need the same logic for `writeHelper`.


Resolving patch:
diff --git a/conftest.py b/conftest.py
--- a/conftest.py
+++ b/conftest.py
@@ -40,6 +40,10 @@
     pyodide.pyimport("pyodide_js._api")
 """
 
+only_node = pytest.mark.xfail_browsers(
+    chrome="node only", firefox="node only", safari="node only"
+)
+
 
 def pytest_addoption(parser):
     group = parser.getgroup("general")


Test patch:
diff --git a/src/tests/test_streams.py b/src/tests/test_streams.py
--- a/src/tests/test_streams.py
+++ b/src/tests/test_streams.py
@@ -1,7 +1,7 @@
 import pytest
 from pytest_pyodide import run_in_pyodide
 
-from conftest import strip_assertions_stderr
+from conftest import only_node, strip_assertions_stderr
 
 
 @pytest.mark.skip_refcount_check
@@ -576,3 +576,49 @@ def test_custom_stdout_interrupts(selenium, method):
             pyodide.setStdout();
             """
         )
+
+
+@only_node
+@run_in_pyodide
+def test_node_eagain(selenium):
+    from pyodide.code import run_js
+
+    result = run_js(
+        """
+        pyodide.setStdin({
+            i: 0,
+            stdin() {
+                this.i ++;
+                if (this.i < 3) {
+                    throw {code: "EAGAIN"};
+                }
+                this.i = 0;
+                return "abcdefg";
+            }
+        });
+        let result = [];
+        pyodide.setStdout({
+            i: 0,
+            write(a) {
+                this.i ++;
+                if (this.i < 3) {
+                    throw {code: "EAGAIN"};
+                }
+                this.i = 0;
+                result.push(new TextDecoder().decode(a));
+                return a.length;
+            }
+        });
+        result
+        """
+    )
+    try:
+        assert input() == "abcdefg"
+        print("hi there!")
+        assert result[0] == "hi there!\n"
+    finally:
+        run_js(
+            """
+            pyodide.setStdin();
+            """
+        )