github-actions[bot] commented on code in PR #67114:
URL: https://github.com/apache/doris/pull/67114#discussion_r3860859907
##########
be/src/exec/sink/writer/paimon/jni_paimon_write_backend.cpp:
##########
@@ -135,15 +190,13 @@ Status JniPaimonWriteBackend::close() {
env->CallVoidMethod(_jni_writer_obj, _close_id);
close_status = _check_jni_exception(env, "close PaimonJniWriter");
Review Comment:
[P2] Bound JNI locals for the initial close too.
The new `PushLocalFrame` fixes the previously reported quarantine-retry
site, but this distinct first-close path still calls `GetJniExceptionMsg(...,
true)` through `_check_jni_exception` without a frame. That helper deletes the
throwable but not its `msg` and optional `stack` local jstrings. Because
successful recovery now admits later writers on reused TLS-attached native
workers, successive first-close failures can accumulate those locals over the
BE lifetime. Frame this call or release the helper's temporary references, and
cover repeated fail-recover-readmit cycles.
##########
be/src/exec/sink/writer/paimon/jni_paimon_write_backend.cpp:
##########
@@ -46,31 +46,93 @@ namespace doris {
namespace {
constexpr std::string_view PAIMON_JNI_WRITER_IO_TMP_DIR =
"paimon_jni_writer_io_tmp";
-std::atomic<bool>& paimon_jni_close_failed() {
- static std::atomic<bool> failed {false};
- return failed;
-}
+struct PaimonJniCloseState {
+ // The global writer reference keeps both the Java object and its defining
class alive. The
+ // cached method ID therefore remains valid until release_writer_ref()
deletes this reference.
+ jobject writer_obj = nullptr;
+ jmethodID recover_and_close_id = nullptr;
+ std::unique_ptr<PaimonJniMemoryManager> memory_manager;
-std::mutex& retained_memory_managers_mutex() {
+ Status retry_cleanup(JNIEnv* env) const {
+ if (writer_obj == nullptr) {
+ return Status::OK();
+ }
+ if (recover_and_close_id == nullptr) {
+ return Status::InternalError("PaimonJniWriter.recoverAndClose
method is unavailable");
+ }
+
+ if (env->PushLocalFrame(16) != JNI_OK) {
+ env->ExceptionClear();
+ return Status::InternalError(
+ "Failed to create a JNI local frame for Paimon cleanup
recovery");
+ }
+ Defer pop_local_frame {[&] { env->PopLocalFrame(nullptr); }};
+ env->CallVoidMethod(writer_obj, recover_and_close_id);
+ return Jni::Env::GetJniExceptionMsg(env, true, "JNI exception retrying
Paimon cleanup: ");
+ }
+
+ void release_writer_ref(JNIEnv* env) {
+ if (writer_obj != nullptr) {
+ env->DeleteGlobalRef(writer_obj);
+ writer_obj = nullptr;
+ }
+ }
+};
+
+std::mutex& quarantined_close_states_mutex() {
static auto* mutex = new std::mutex();
return *mutex;
}
-std::vector<std::unique_ptr<PaimonJniMemoryManager>>&
retained_memory_managers() {
- static auto* managers = new
std::vector<std::unique_ptr<PaimonJniMemoryManager>>();
- return *managers;
+std::vector<PaimonJniCloseState>& quarantined_close_states() {
+ // Deliberately process-lifetime storage: if cleanup never succeeds,
retaining the writer and
+ // native pages at process exit is safer than destroying memory still used
by Java tasks.
+ static auto* states = new std::vector<PaimonJniCloseState>();
+ return *states;
}
-void retain_memory_after_failed_close(std::unique_ptr<PaimonJniMemoryManager>
manager) {
+void quarantine_failed_close(jobject writer_obj, jmethodID
recover_and_close_id,
+ std::unique_ptr<PaimonJniMemoryManager>
memory_manager) {
// An unconfirmed Java close means a background Paimon task may still
reference this manager's
- // native pages. Quarantine the manager and stop admitting new writers so
repeated failures
- // cannot accumulate process-lifetime native memory without a bound.
- paimon_jni_close_failed().store(true, std::memory_order_release);
- if (manager == nullptr) {
- return;
+ // native pages. Keep the Java writer reachable as well, so a later open
can finish deferred
+ // abort and close cleanup before admitting more writers instead of
requiring a BE restart.
+ std::lock_guard<std::mutex> lock(quarantined_close_states_mutex());
+ quarantined_close_states().push_back(
Review Comment:
[P1] Make quarantine publication allocation-safe.
Both close-failure paths have already exchanged away the global writer and
moved `_memory_manager` into this by-value call. First initialization of the
heap-backed mutex/vector, or growth in this `push_back`, can throw
`std::bad_alloc` on the exact memory-pressure path. Unwinding then destroys the
manager and releases all native pages while the raw global Java writer was
neither queued nor deleted and may still have Paimon tasks using those pages;
destructor-driven close can also terminate if this escapes. Allocate/fence the
storage before transferring ownership, or provide a no-fail fallback that
deliberately retains both owners, and failure-inject initial allocation and
vector growth.
##########
fe/be-java-extensions/paimon-connector/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java:
##########
@@ -617,22 +718,63 @@ private void clearWriterState() {
}
private void abortWriter() throws Exception {
- try {
- List<CommitMessage> messages = preparedCommitMessages;
- if (messages.isEmpty() && writer != null) {
- messages = prepareCommitMessages();
+ if (writer == null && preparedCommitMessages.isEmpty()) {
+ abortPending = false;
+ deferAbortAfterOutOfMemory = false;
+ return;
+ }
+
+ abortPending = true;
+ if (deferAbortAfterOutOfMemory && preparedCommitMessages.isEmpty()) {
+ LOG.warn("Deferring Paimon prepare-and-abort cleanup after OOM");
+ return;
+ }
+ finishPendingAbort();
+ closeWriter();
+ }
+
+ private void finishPendingAbort() throws Exception {
+ if (!abortPending) {
+ return;
+ }
+
+ List<CommitMessage> messages = preparedCommitMessages;
+ if (messages.isEmpty() && writer != null) {
+ messages = prepareCommitMessages();
Review Comment:
[P1] Do not retry a non-resumable whole-writer prepare.
In Paimon 1.4.2, `AbstractFileStoreWrite.prepareCommit()` visits buckets
sequentially into a method-local result, while append/postpone bucket writers
destructively clear their pending increments. If a later bucket throws, earlier
messages never return; similarly, an OOM in Doris's post-return `new
ArrayList<>(messages)` loses the already-drained complete result. This retry
then sees those buckets empty and cannot pass their file paths to abort. Use an
atomic/resumable preparation contract or durably capture each increment before
advancing, and test a real multi-bucket partial failure with file deletion
assertions.
##########
fe/be-java-extensions/paimon-connector/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java:
##########
@@ -617,22 +718,63 @@ private void clearWriterState() {
}
private void abortWriter() throws Exception {
- try {
- List<CommitMessage> messages = preparedCommitMessages;
- if (messages.isEmpty() && writer != null) {
- messages = prepareCommitMessages();
+ if (writer == null && preparedCommitMessages.isEmpty()) {
+ abortPending = false;
+ deferAbortAfterOutOfMemory = false;
+ return;
+ }
+
+ abortPending = true;
+ if (deferAbortAfterOutOfMemory && preparedCommitMessages.isEmpty()) {
+ LOG.warn("Deferring Paimon prepare-and-abort cleanup after OOM");
+ return;
+ }
+ finishPendingAbort();
+ closeWriter();
+ }
+
+ private void finishPendingAbort() throws Exception {
+ if (!abortPending) {
+ return;
+ }
+
+ List<CommitMessage> messages = preparedCommitMessages;
+ if (messages.isEmpty() && writer != null) {
+ messages = prepareCommitMessages();
+ }
+ if (!messages.isEmpty()) {
+ InnerTableCommit committer = table.newCommit(commitUser);
+ try {
+ committer.abort(messages);
+ } finally {
+ committer.close();
}
- if (!messages.isEmpty()) {
- InnerTableCommit committer = table.newCommit(commitUser);
- try {
- committer.abort(messages);
- } finally {
- committer.close();
- }
+ }
+
+ preparedCommitMessages = Collections.emptyList();
+ abortPending = false;
+ deferAbortAfterOutOfMemory = false;
+ }
+
+ private void recordOutOfMemoryFailure(Throwable failure) {
+ if (containsOutOfMemory(failure)) {
+ deferAbortAfterOutOfMemory = true;
+ }
+ }
+
+ static boolean containsOutOfMemory(Throwable failure) {
+ Throwable current = failure;
+ for (int depth = 0; current != null && depth < 32; depth++) {
+ if (current instanceof OutOfMemoryError || current instanceof
OutOfMemoryException) {
Review Comment:
[P1] Preserve native OOM identity for the deferral decision.
The production page callback maps Doris `QUERY_MEMORY_EXCEEDED` and native
allocator failures to a cause-less Java `RuntimeException`, which this loop
cannot recognize; C++ Arrow conversion/export OOM can also fail before
`writeArrow` sets the Java flag. The common C++ abort path then immediately
invokes allocation-heavy prepare/abort under the same pressure this state
machine is intended to defer. Propagate a machine-detectable memory-exhaustion
status across both native paths and add callback/conversion-level tests proving
prepare is postponed until recovery.
##########
fe/be-java-extensions/paimon-connector/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java:
##########
@@ -617,22 +718,63 @@ private void clearWriterState() {
}
private void abortWriter() throws Exception {
- try {
- List<CommitMessage> messages = preparedCommitMessages;
- if (messages.isEmpty() && writer != null) {
- messages = prepareCommitMessages();
+ if (writer == null && preparedCommitMessages.isEmpty()) {
+ abortPending = false;
+ deferAbortAfterOutOfMemory = false;
+ return;
+ }
+
+ abortPending = true;
+ if (deferAbortAfterOutOfMemory && preparedCommitMessages.isEmpty()) {
+ LOG.warn("Deferring Paimon prepare-and-abort cleanup after OOM");
+ return;
+ }
+ finishPendingAbort();
+ closeWriter();
+ }
+
+ private void finishPendingAbort() throws Exception {
+ if (!abortPending) {
Review Comment:
[P1] Abort prepared output after a later close failure.
`PaimonTableWriter::close()` aborts only before backend close. If prepare
succeeds but writer/index/IO/allocator close then fails, C++ suppresses
publication and drops its local message copy, while Java never marked this
successful prepare as `abortPending`. Recovery therefore takes this return,
eventually clears `preparedCommitMessages`, and never calls
`InnerTableCommit.abort` for the unpublished files. Preserve an abort
obligation for prepared-but-unpublished output until abort and dependent
cleanup both succeed, and test with a non-empty message plus an injected close
failure.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]