Skip to content

Native insert_columns(parallel=True) takes the columnar entry (closes #325) - #328

Merged
tae898 merged 5 commits into
release/26.11.1from
fix/325-native-columns-parallel
Nov 10, 2026
Merged

tae898 merged 5 commits into
release/26.11.1from
fix/325-native-columns-parallel

Conversation

@tae898

@tae898 tae898 commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

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 is release/26.11.1, for the wheels-only 26.11.1.post1.

What changes: the columnar entry arcadedb_n_insert_colp takes "parallel": true in its header. The library builds each record from the column buffers (the same setRow code as the synchronous call) and hands it to db.async().createRecord with a failure token, as DocumentBatcher.insertColumnsParallel does on the JVM wheel. No transaction is opened, committed or rolled back there and commit_every does not apply, which is what the parallel row path already did. The Python side makes the failure token before the crossing (a new token operation of arcadedb_n_async) and passes its id in the header ("fid"), then waits for the writers and reads the failures through the same code as insert_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, and ARCADEDB_NATIVE_BULK=0 still turns the columnar path off for parallel calls too. An edge type is refused with the same message as before.

insert_arrow and insert_dataframe with parallel=True on a document type had the same fall back (the columns became Python values and went to insert_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 refuse parallel=True there, 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, one setRow shared by both modes) and NativeExtras.java (the failure token that insert_parallel already used, now a small class used by both, with the token operation 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 one arcadedb_n_insert_colp crossing and no insert_parallel chunk (it fails on the old code, which made the row call); the stored records, with value and Python type per property, equal those of insert_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 test tests/test_cross_wheel_golden.py is 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, any BaseException) during the crossing, or between the chunks of insert_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 only ArcadeDBError and 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_parallel now reads every token even when the wait itself raises; before, the failure counters of an unread token (ASYNC_FAILS and ASYNC_FIRST, which hold a Throwable) lived for the life of the process, and the row path of insert_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 asserts stored + reported failed >= n (the count is parsed from the message) and stored == number of distinct ids; a load of threads * 10240 + 1 rows, 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, then KeyboardInterrupt) leaves is_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_every rows) 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 guarantees stored + 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

tae898 and others added 2 commits November 9, 2026 18:53
…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>
@tae898

tae898 commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI evidence for 25bceaa8d, dispatched on this branch (a draft PR skips the platform legs, and the PR runs use the latest jars): Test native version=26.11.1.post1 https://github.com/ArcadeData/arcadedb-python/actions/runs/42597223330, Test JVM arcadedb-tag=26.11.1 https://github.com/ArcadeData/arcadedb-python/actions/runs/42597226741, Test Python Examples arcadedb-tag=26.11.1 https://github.com/ArcadeData/arcadedb-python/actions/runs/42597230427. Results will be added here.

🤖 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>
@tae898

tae898 commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

I applied an independent review to this PR (new head c24f154). All four findings were right and are fixed: the columnar parallel path now waits for the writers and reads the failure token on any BaseException (an interrupt included), _finish_parallel reads every token even when the wait raises (the row path leaked the same entries, fixed there too), the failure text says how many rows were handed over, and the in-transaction message of insert_table(parallel=True) no longer claims the rows are uncommitted. The token is now made by the caller and its id travels in the header, so the Java side never drops it. Tests for each are in tests/test_native_columnar_parallel_review_fixes.py; the PR body lists the behaviour changes. I could not run anything locally, so CI is the evidence: native, jvm, examples on the fork.

…'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>
@tae898 tae898 self-assigned this Nov 9, 2026
@tae898
tae898 merged commit dc55dec into release/26.11.1 Nov 10, 2026
19 checks passed
@tae898
tae898 deleted the fix/325-native-columns-parallel branch November 10, 2026 02:23
@github-actions github-actions Bot added this to the 26.12.1 milestone Nov 10, 2026
abhirajadhikary06 pushed a commit to abhirajadhikary06/arcadedb-python that referenced this pull request Nov 10, 2026
…th its stack ArcadeData#328 and ArcadeData#333)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant