Repository navigation
JVM build: Java System.out/System.err go to a file the library owns when the console is off (Windows data corruption) - #88
Conversation
…owns when the console is off On Windows the runtime caches the OS handle of stdout and stderr at start-up. A host that redirects fd 1 or 2 later closes it, a database page file opened afterwards can get the same handle value, and the next System.out or System.err write lands in the page file (run 40250615309: Invalid position 793803132 (size=65536)). The JVM twin of #78, which removed the JUL console handler but not the other Java writers. start_jvm() now points both streams at arcadedb-stdio.log in the log directory right after startJVM, when the console is off (Windows default, or ARCADEDB_LOG_CONSOLE=0). The test repeats redirect, index, reopen and query ten times in one child, prints a Java probe after every step and scans every database file. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Back to draft: CI is green, but a Windows loop that runs |
…o the file the library owns System.setOut and System.setErr do not reach the runtime's own warnings: the runtime prints them to System.initialErr, a copy of the stream over the cached stderr handle that it makes before application code runs. The JEP 498 warning for sun.misc.Unsafe (jvector causes it when it unmaps a graph file on the first reopen of a vector index) therefore still went to the cached handle on Windows. After a host's dup2 onto fd 2 closed that handle, a page file opened later could get the same handle value, and the four warning lines landed in it (Invalid position 793803132 (size=65536), one run in 20 to 60 on the Windows loop). jvm._redirect_java_stdio now also sets java.lang.System.initialErr (a plain static field; --add-opens=java.base/java.lang is a default of this package) to the same stream. The redirect tests trigger the warning at once (jvector's SimpleMappedReader) and check that it is in arcadedb-stdio.log and nowhere else; they fail on Linux without this change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Correction to my earlier comment: the probe text in the redirect target came from the control legs (main's |
… 498 trigger and probes) #88 made the child of the redirect tests call jpype.JClass("java.lang.System") for the Java stream probes and the SimpleMappedReader$Supplier trigger of the JEP 498 Unsafe warning. The native build has no JVM, so four tests of the file failed in the native parity run (job 121437057831). The redirect hygiene is shared behaviour, so the tests stay on both builds; only the Java-level probes, the trigger and the UNSAFE_WARNING assertions are gated on arcadedb.__backend__ == "jvm". Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
What
The JVM twin of #78. #78 removed the JUL console handler on Windows, but Java-level writes to
System.outandSystem.err, and the runtime's own warnings, still use the OS handle that the runtime cached at start-up. With the console off (the default on Windows, orARCADEDB_LOG_CONSOLE=0),start_jvm()now replacesSystem.out,System.errandSystem.initialErr, right afterstartJVM, with onePrintStreamonarcadedb-stdio.login the log directory (_logdir.stdio_log_path()). The file is opened by the library, so no cached handle is involved, whatever the host does with fd 1 and 2.System.initialErris the part that the first version of this PR missed. It is the copy ofSystem.errthat the runtime makes before any application code runs, andSystem.setErrdoes not change it. The runtime prints its own warnings to it, and one of them is printed in every run of the vector tests (see Root cause).Root cause
The first version of this PR (head 2e33a26) still failed on Windows in about one run in 20 to 60 (the temporary loop of run 40252312732: 3 failures in 50 runs at 1 repeat, 1 in 50 at 10 repeats; the
ARCADEDB-STDIO-PROBElines in the redirect target in that loop's log came from the control legs that run main'sjvm.py, not from the fixed legs). Every failure of the fixed legs was the sameInvalid position 793803132 (size=65536): the same number each time, so the same text each time.A temporary Windows diagnostic (no PR, deleted; run 40264486763 builds the wheel, run 40265118569 loops) found the writer in two ways:
Invalid position 793803132), the scan of the database directory founddb0\Doc_0.1.65536.v0.bucketstarting with the four lines below, at offsets 0, 75 and 297. With no redirect (plain, 120 runs) nothing failed.dup2it opens a file untilCreateFilereturns the value thatFileDescriptor.errcached, and keeps it. In 114 of 120 and 117 of 120 runs that canary held exactly these four lines (in the other 9 runs the canary could not take the value).That is the JEP 498 warning (JDK 24 and later): jvector unmaps a graph file with
Unsafe.invokeCleanerwhen the vector index is reopened, and the runtime prints the warning once.sun.misc.Unsafe.logprints it tojdk.internal.misc.VM.initialErr(), which returnsSystem.initialErr, not toSystem.err(the JEP 472 warning for restricted methods injava.lang.Moduledoes the same).System.setErrleaves it on the cached handle. When the host'sdup2onto fd 2 has closed that handle and a page file opened later gets the same value, the four lines land in the page file.Nothing else in
java.baseholds the cached handle: a scan of the module findsFileDescriptor.outandFileDescriptor.erronly inSystem(which makes the three streams) and inJdkConsoleImpl.Evidence of the first version
Dispatch run 40250615309 (
test-jvm.ymlon the PR #87 branchwindows-default-capture), job 121341603694, Windows Python 3.13:tests/test_log_redirect.py::test_redirect_with_the_console_off_sends_nothing_to_the_redirect_targetfailed withArcadeDBError: Reading the result set failed: java.lang.IllegalArgumentException: Invalid position 793803132 (size=65536), which is text read as a page position, the #78 signature. The other Windows legs of that run, and 3 of the 4 legs of the other run, passed: it is intermittent.The
WARNING: Using incubator modules: jdk.incubator.vectorin that failure message is not a leak into the redirect target. The child starts the JVM before it redirects, so the runtime prints that line to the parent's pipe (proc.stderr=b'WARNING: Using incubator modules: jdk.incubator.vector\r\n'in the log), and the old assertion message joined stdout, stderr and the redirect target into one text. The test needs no weaker expectation: the new message names each part. A host that redirects fd 2 before it starts the JVM does get that one line in its own target, which is its choice. Nothing the library prints afterstartJVMcan reach it.Design
ARCADEDB_LOG_CONSOLE=0asks for the console off, on any platform. The default on Linux and macOS is unchanged: the runtime writes to fd 1 and 2 by number there and follows a redirect. Tying it to the console setting (not tosys.platform) keeps one rule, and lets the Linux CI exercise the code.arcadedb-stdio.log, notarcadedb.log: the engine's file handler owns and rotates that one, and on Windows renaming a file that another handle holds open fails. Several processes that share a log directory append to the same file; a write is small.ARCADEDB_LOG_CONSOLE=1on Windows keeps the runtime's own streams, so the risk stays; it is documented (jvm.start_jvm,_logdir,docs/.../how-to/logs.adoc). ReopeningCONOUT$is not done: it would ignore a redirect that the host set up on purpose before the start.System.initialErris replaced by reflection (System.class.getDeclaredField("initialErr"), a plain static field).--add-opens=java.base/java.lang=ALL-UNNAMEDis a default of this package already (_build_jvm_args). If it cannot be set (a caller's own--add-opensfor another package set, a future JDK that renames the field),start_jvm()logs a warning and the runtime's own warnings keep the cached handle.Tests
tests/test_log_redirect.pyis stronger on every build:The child repeats redirect, create index, build graph, close, reopen and query
ARCADEDB_REDIRECT_REPEATStimes in one process (default 10), and after every step prints a probe through Java'sSystem.outandSystem.err. A probe is a write through the cached handle that lands in a page file whenever a database file owns the handle value, with no log line needed.Every file under the data directory is scanned for engine log lines, probes and the runtime warning.
With the console off, the redirect target must hold no log line and no probe, and
arcadedb-stdio.logmust hold every probe, from both streams (exact count).With the console on, the probes are on the console and there is no stdio file.
The child also unmaps a file with jvector's
SimpleMappedReader.Supplierright after the redirect, so the runtime prints the JEP 498 warning at once (it also prints it at the first reopen). The warning must not be in the redirect target, the console or any database file, and must be inarcadedb-stdio.log.Without the fix the console-off redirect test and
test_console_log_can_be_turned_offfail on Linux too (the probes go to fd 1 and 2); without theSystem.initialErrpart they fail on Linux on the warning alone (checked against 2e33a26); with it all 21 tests pass.Checks run
black, isort
--profile black,tests/test_log_dir.py,tests/test_log_redirect.pyandtests/test_jvm_args.pyon Linux (Python 3.12, JRE 25.0.5, engine and jars of a 26.11.1.dev0 JVM wheel). The Windows proof (bar: 0 failures, no text in any database file):src/arcadedb/*.py): Windows Python 3.13,test_redirecting_fd_1_and_2_leaves_the_database_files_clean,test_redirect_with_the_console_off_sends_nothing_to_the_redirect_targetandtest_console_log_can_be_turned_offper iteration, pytest's default capture. 100 iterations at 1 repeat per child, twice (200), and 50 at 10 repeats per child, twice (100): 0 failures in 300 iterations (the version without theSystem.initialErrpart failed about 1 in 20 to 60). The canary legs (60 iterations with the stale handle taken over by a canary file, 60 without) show the canary empty and the redirect target empty in all 120.test-jvm.ymldispatched twice on this branch at 9a35d13: runs 40267979586 and 40267982374.🤖 Generated with Claude Code