Repository navigation
Native insert_columns(parallel=True) takes the columnar entry (closes #325) - #328
Conversation
…325) A parallel call skipped the columnar fast path and went through one dict per row and one JSON text per chunk of 10,000 rows. The columnar entry arcadedb_n_insert_colp now takes "parallel": true in its header: the image builds the records from the column buffers and hands them to the async writers (db.async().createRecord with a failure token), as DocumentBatcher.insertColumnsParallel does on the JVM wheel. The Python side waits for the writers and reads the failures exactly as insert_many(parallel=True) does (_finish_parallel, shared). Rows the buffers cannot carry still take the row path. The native library is rebuilt from the unchanged engine commit. Tests (native only): a spy proves one colp crossing and no insert_parallel chunk; the stored records equal those of insert_many(parallel=True); nulls, vertex types, rejected records, an edge type, a missing type, the fall back and the off switch. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… columnar entry (refs #325) A document load with parallel=True left the Arrow columns as Python values and went to insert_many(parallel=True). It now keeps the columns as they are for the columnar entry, which hands the records to the async writers; a batch the buffers cannot carry still takes the row path. A test spies on the entry points and compares the stored records with the row path of the old code. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
CI evidence for 🤖 Generated with Claude Code |
… after black wrapped it Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…loss (review of #328) The failure token is now made by the caller (arcadedb_n_async "token") and its id travels in the colp header, so the caller holds the id whatever happens to the crossing. An interrupt in it (or between the chunks of the row path) waits for the writers and reads the tokens before it propagates; a wait that raises still reads every token (the entries of an unread token lived forever); the error of a failed handover says how many rows were handed over and that some may have been stored. insert_table(parallel=True) no longer says the earlier batches were written in the caller's transaction (the writers commit them, a rollback does not remove them). Tests: duplicate keys (stored + reported >= n, stored == distinct), the writers' batch boundary, nothing lands after the call, an interrupt in the crossing and in the row path, a failing wait, the in-transaction message. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
I applied an independent review to this PR (new head |
…'s open batch (CI of the first candidate) The first candidate run showed stored 1000 of 2950 distinct keys: a writer that meets a duplicate key rolls back its whole open batch (up to commit_every rows), so the valid rows of that batch are abandoned and reported too. stored == distinct was the wrong invariant. Assert stored + reported >= n, stored <= distinct, only sent ids, none stored twice. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…th its stack ArcadeData#328 and ArcadeData#333) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
For users nothing changes in the values or the errors: on the native wheel
insert_columns(..., parallel=True)now sends the columns to the library in one call instead of turning them into one dict per row and one JSON text per 10,000 rows, and the same records are stored. Closes #325 (part of #315). Base isrelease/26.11.1, for the wheels-only26.11.1.post1.What changes: the columnar entry
arcadedb_n_insert_colptakes"parallel": truein its header. The library builds each record from the column buffers (the samesetRowcode as the synchronous call) and hands it todb.async().createRecordwith a failure token, asDocumentBatcher.insertColumnsParalleldoes on the JVM wheel. No transaction is opened, committed or rolled back there andcommit_everydoes not apply, which is what the parallel row path already did. The Python side makes the failure token before the crossing (a newtokenoperation ofarcadedb_n_async) and passes its id in the header ("fid"), then waits for the writers and reads the failures through the same code asinsert_many(parallel=True)(_finish_parallel, split out of_insert_many_parallel), so the error text, the "rows may have been stored" rule and the failed-record count are the same. A column set the buffers cannot carry (mixed types, nested values, date-times) still takes the row path, andARCADEDB_NATIVE_BULK=0still turns the columnar path off for parallel calls too. An edge type is refused with the same message as before.insert_arrowandinsert_dataframewithparallel=Trueon a document type had the same fall back (the columns became Python values and went toinsert_many(parallel=True)), so they now keep their columns for the same entry (dataframe.py,_insert_documents_native); a batch the buffers cannot carry still takes the row path. Vertex types still refuseparallel=Truethere, as before.This is Java glue: the native library is rebuilt from the unchanged engine commit, no engine file is touched. The two glue files are
BulkOps.java(the parallel branch, onesetRowshared by both modes) andNativeExtras.java(the failure token thatinsert_parallelalready used, now a small class used by both, with thetokenoperation that makes one). Memory: the call no longer builds the per-row dict list and the JSON texts, and the records are handed over as they are built (the same hand-over as the JVM wheel's parallel call); I did not measure the peak.Tests (native only,
tests/test_native_columnar_parallel.py): a spy proves that a parallel call makes onearcadedb_n_insert_colpcrossing and noinsert_parallelchunk (it fails on the old code, which made the row call); the stored records, with value and Python type per property, equal those ofinsert_many(parallel=True)with dict rows for a document type, a vertex type and a column set with nulls and non-ASCII text; a duplicate key and a value the declared type refuses are reported after the wait; edge type, missing type, mixed-type fallback, the off switch, empty columns and the unchanged synchronous call are covered. The golden testtests/test_cross_wheel_golden.pyis not touched.No speed claim is made here. The measurement is the untimed profile study of the ingest shapes and a later laptop A/B on this change. I could not run any test locally (CI only); the CI runs are linked in the first comment.
Changes after an independent review of this PR, each one a behaviour change worth reading. An interrupt (
KeyboardInterrupt, anyBaseException) during the crossing, or between the chunks ofinsert_many(parallel=True), now waits for the writers and reads (so removes) the failure tokens before it propagates, as the row path already waited; before, the columnar path caught onlyArcadeDBErrorand left the writers running with no wait. Because the caller owns the token and sends its id in the header, an interrupt that lands after the records are handed over cannot lose the id._finish_parallelnow reads every token even when the wait itself raises; before, the failure counters of an unread token (ASYNC_FAILSandASYNC_FIRST, which hold aThrowable) lived for the life of the process, and the row path ofinsert_many(parallel=True)had the same leak, which is fixed there too. A failure while the image hands records over now says how many it had handed over ("after handing N of M rows to the async writers (those may have been stored)") and the Python message adds the failed records the writers had already counted; Java no longer drops the token, so those counts are not discarded.insert_table(parallel=True)inside a caller's transaction no longer says the earlier batches were "written in your transaction, not committed" (a pre-existing message that was false for async writes): the writers commit on their own, a rollback of the caller's transaction does not remove those rows, and the message now says that.Review tests (native only,
tests/test_native_columnar_parallel_review_fixes.py): a duplicate-key load assertsstored + reported failed >= n(the count is parsed from the message) andstored == number of distinct ids; a load ofthreads * 10240 + 1rows, one past a full batch for every writer, stores every row and the count is unchanged after the call returns; an interrupt in the crossing (the real call runs, thenKeyboardInterrupt) leavesis_processing()false and the token read, and so does one between the chunks of the row path; a wait that raises still reads the tokens; the in-transaction message does not promise a rollback and the committed rows survive one.In parallel mode a duplicate key abandons the open batch of the failing writer: the async writers roll back their whole open batch (up to
commit_everyrows) when one record fails, so the valid rows of that batch are not stored and are reported in the error with the failed one. A parallel load therefore guaranteesstored + reported failed >= rows, no row stored twice and only sent rows stored, not that every distinct key is stored; the review test asserts exactly that (the first CI run showed 1,000 of 2,950 distinct keys stored for a load with 50 repeated keys).🤖 Generated with Claude Code