Skip to content

GH-51663: [C++][Gandiva] Race condition with Gandiva expressions - #51665

Merged
kou merged 6 commits into
apache:mainfrom
lriggs:lockFixUpstream
Oct 23, 2026
Merged

kou merged 6 commits into
apache:mainfrom
lriggs:lockFixUpstream

Conversation

@lriggs

@lriggs lriggs commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Rationale for this change

I originally identified this issue in arrow-java and fixed it on the arrow-java side with a coarse grained lock on the Projector and Filter Make methods. That fixed the issue but adds some undesirable overhead in certain multi threaded workflows.
This issue captures the deeper problem that is occurring in the C++ side of Gandiva and was not touched by my previous fix though it is prevented from happening. I have a proposed solution for this and believe it is a better overall solution.

Projector::Make() and Filter::Make() read the shared expression cache once to decide the is_cached status and then call SetLLVMObjectCache(). That performs its own second, unsynchronized read of the same key before pre-loading a cached object into the LLJIT.

Those two reads could disagree. If another thread compiling the identical (schema, expressions, selection vector mode, configuration) tuple inserted between them, the first thread would:

  1. take the is_cached == false path, generating expr_0_0 into its IR module, and
  2. also see a hit on the second read and addObjectFile() a cached object that defines expr_0_0 as well.

Both then call JITDylib::define for the same symbol in the same JITDylib, and ORC's duplicate-symbol detection fires:

CodeGenError in Gandiva: Failed to add IR module to LLJIT:
In gdv_module_..., duplicate definition of symbol 'expr_0_0'

What changes are included in this PR?

Thread the single cache-lookup result (prev_cached_obj) that already determined is_cached directly into SetLLVMObjectCache(), instead of letting it perform an independent second lookup.

Engine::SetLLVMObjectCache() and LLVMGenerator::SetLLVMObjectCache() now take the resolved std::shared_ptr<llvm::MemoryBuffer> rather than a GandivaObjectCache&. Engine::SetLLVMObjectCache() passes a copy of the buffer to addObjectFile(), as the old GandivaObjectCache::getObject() did, because the shared cache still owns the original buffer.

No new locking — this removes the window rather than serializing around it.

Are these changes tested?

This adds a new deterministic regression test, cpp/src/gandiva/tests/cache_race_test.cc, which is part of gandiva-projector-test.

The test doesn't depend on thread timing. It registers an ExportedFuncsBase hook that inserts an entry for the key being built into the shared cache during Engine::Init(). That is after Make()'s first cache lookup and before the (pre-fix) second lookup in SetLLVMObjectCache(), so the race window is reproduced on every run. It covers both Projector::Make() and Filter::Make(), and checks that the built projector/filter evaluates correctly.

Verified the tests fail with the duplicate-symbol error without this fix.

Verified internally with our product stress tests after removing the arrow-java side of the fix. Once this change is in C++ I can make the java change to remove the synchronization.

Are there any user-facing changes?

This PR includes breaking changes to public APIs.

Engine::SetLLVMObjectCache and LLVMGenerator::SetLLVMObjectCache are GANDIVA_EXPORT so this changes an exported signature. In-tree there are no other callers, and the Gandiva Java side JNI only uses Projector::Make/Filter::Make, which are unchanged.

Was AI used for this PR?

AI was used to diagnose the problem, write code and tests over several iterations before being reviewed by two human developers. A human ran several manual and regression tests against the change. One human reviewed and heavily edited the PR description and also wrote some himself.

PR code and description written by:

  • Human
  • AI

Reviewed before submission by:

  • Human
  • AI
  • Not reviewed

) and fixed it on the arrow-java side with a coarse grained lock on the Projector and Filter Make methods. That fixed the issue but adds some undesirable overhead in certain multi threaded workflows.

This issue captures the deeper problem that is occurring in the C++ side of Gandiva and was not touched by my previous fix though it is prevented from happening. I have a proposed solution for this and believe it is a better overall solution.

Projector::Make() and Filter::Make() read the shared expression cache once to decide
the is_cached status and then call SetLLVMObjectCache(). That performs its own second, unsynchronized
read of the same key before pre-loading a cached object into the LLJIT.

Those two reads could disagree. If another thread compiling the identical
(schema, expressions, selection vector mode, configuration) tuple inserted between them, the
first thread would:

take the is_cached == false path, generating expr_0_0 into its IR module, and
also see a hit on the second read and addObjectFile() a cached object that defines
expr_0_0 as well.
Both then call JITDylib::define for the same symbol in the same JITDylib, and ORC's
duplicate-symbol detection fires:

CodeGenError in Gandiva: Failed to add IR module to LLJIT:
In gdv_module_..., duplicate definition of symbol 'expr_0_0'

C++, Gandiva

Thread the single cache-lookup result (prev_cached_obj) that already determined is_cached
directly into SetLLVMObjectCache(), instead of letting it perform an independent second lookup.
Engine::SetLLVMObjectCache and LLVMGenerator::SetLLVMObjectCache now take the resolved
shared_ptr<llvm::MemoryBuffer> rather than a GandivaObjectCache&.

No new locking — this removes the window rather than serializing around it.

New cpp/src/gandiva/tests/concurrent_make_test.cc.
Verified tests fail with the duplicate-symbol error without this fix.

Verified internally with our product stress tests after removing the
arrow-java side of the fix. Once this change is in C++ I can make
the java change to remove the synchronization.
Comment thread cpp/src/gandiva/tests/concurrent_make_test.cc Outdated
@github-actions github-actions Bot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Oct 7, 2026
Comment thread cpp/src/gandiva/tests/concurrent_make_test.cc Outdated
Comment thread cpp/src/gandiva/tests/concurrent_make_test.cc Outdated

@dmitry-chirkov-dremio dmitry-chirkov-dremio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change looks good and I’ll approve once my test-related comments are addressed.

@dmitry-chirkov-dremio dmitry-chirkov-dremio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Nit: GH-601 reference is from arrow-java - not sure what are the policies on cross-repo references

@dmitry-chirkov-dremio

Copy link
Copy Markdown
Contributor

@kou this is good to go.

@kou

kou commented Oct 21, 2026

Copy link
Copy Markdown
Member

Could you update the PR description before we merge this?

@lriggs

lriggs commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Could you update the PR description before we merge this?

Sorry, what update? If it's about the GH issues, the link in the description goes to arrow-java correctly. Although at this moment apache/arrow-java#601 gives an error for some reason, though its the right URL.

@kou

kou commented Oct 23, 2026

Copy link
Copy Markdown
Member

It seems that the latest changes aren't reflected to the description.

I've updated the description. I'll merge this.

@kou
kou merged commit 2db0340 into apache:main Oct 23, 2026
56 of 57 checks passed
@kou kou removed the awaiting committer review Awaiting committer review label Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants