Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion cpp/velox/jni/JniHashTable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,19 @@ void JniHashTableContext::finalize(JNIEnv* env) {
}

jlong JniHashTableContext::callJavaGet(const std::string& id) const {
if (vm_ == nullptr) {
throw gluten::GlutenException("No JVM attached to this process");
}

JNIEnv* env;
if (vm_->GetEnv(reinterpret_cast<void**>(&env), jniVersion) != JNI_OK) {
throw gluten::GlutenException("JNIEnv was not attached to current thread");
}

const jstring s = env->NewStringUTF(id.c_str());
auto result = env->CallStaticLongMethod(jniVeloxBroadcastBuildSideCache_, jniGet_, s);
env->DeleteLocalRef(s);
checkException(env);
return result;
}

Expand Down Expand Up @@ -161,7 +167,16 @@ std::shared_ptr<HashTableBuilder> nativeHashTableBuild(
}

long getJoin(const std::string& hashTableId) {
return JniHashTableContext::getInstance().callJavaGet(hashTableId);
const auto& context = JniHashTableContext::getInstance();
if (!context.hasJavaVM()) {
// No JVM is attached to this process, e.g. the standalone micro benchmark, so the JVM side
// VeloxBroadcastBuildSideCache holding the pre-built hash tables is unreachable. Report a miss
// and let the caller build the hash table from the join's build side input instead.
LOG(WARNING) << "No JVM is attached to this process, cannot look up the pre-built hash table for cache key: "
<< hashTableId << ". A new hash table will be built from the build side input.";
return 0;
}
return context.callJavaGet(hashTableId);
}

size_t serializedHashTableSize(std::shared_ptr<HashTableBuilder> builder) {
Expand Down
8 changes: 8 additions & 0 deletions cpp/velox/jni/JniHashTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ class JniHashTableContext {
return vm_;
}

// Returns false when no JVM is attached to this process, which is the case for the standalone
// micro benchmark and the native unit tests. Callers must not attempt any JNI call then.
bool hasJavaVM() const {
return vm_ != nullptr;
}

ObjectStore* getHashTableObjStore() const {
return hashTableObjStore_.get();
}
Expand Down Expand Up @@ -89,6 +95,8 @@ std::shared_ptr<HashTableBuilder> nativeHashTableBuild(
std::vector<std::shared_ptr<ColumnarBatch>>& batches,
std::shared_ptr<facebook::velox::memory::MemoryPool> memoryPool);

// Returns the handle of the pre-built hash table registered for the given id, or 0 if there is
// none. Safe to call from a process with no JVM attached, in which case it always returns 0.
long getJoin(const std::string& hashTableId);

// Return the exact serialized hash table size for direct buffer allocation.
Expand Down
1 change: 1 addition & 0 deletions cpp/velox/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ if(ENABLE_S3)
endif()
add_velox_test(scoped_timer_test SOURCES ScopedTimerTest.cc)
add_velox_test(row_based_checksum_test SOURCES RowBasedChecksumTest.cc)
add_velox_test(jni_hash_table_test SOURCES JniHashTableTest.cc)
if(BUILD_EXAMPLES)
add_velox_test(my_udf_test SOURCES MyUdfTest.cc)
endif()
Expand Down
53 changes: 53 additions & 0 deletions cpp/velox/tests/JniHashTableTest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "jni/JniHashTable.h"

#include <gtest/gtest.h>

#include "utils/Exception.h"

namespace gluten {

// This test binary, like the micro benchmark, runs without a JVM. JNI_OnLoad is therefore never
// invoked and JniHashTableContext::vm_ stays null.
//
// Looking up the build side of a broadcast hash join used to dereference that null JavaVM
// unconditionally, so converting any substrait plan carrying a non-empty `hashTableId` crashed the
// process. See https://github.com/apache/gluten/issues/12504.
class JniHashTableTest : public ::testing::Test {
protected:
void SetUp() override {
ASSERT_FALSE(JniHashTableContext::getInstance().hasJavaVM())
<< "This test asserts behaviour of a process with no JVM attached";
}
};

TEST_F(JniHashTableTest, getJoinReportsMissWithoutJvm) {
// Must report a miss rather than segfault. SubstraitToVeloxPlanConverter treats 0 as "no
// pre-built table" and falls back to building one from the build side input.
EXPECT_EQ(getJoin("no-such-broadcast-hash-table-id"), 0);
}

TEST_F(JniHashTableTest, callJavaGetThrowsWithoutJvm) {
// The low level accessor reports the missing JVM as a catchable error. GlutenException derives
// from std::runtime_error, so the catch(const std::exception&) around the call site in
// SubstraitToVeloxPlan.cc handles it.
EXPECT_THROW(JniHashTableContext::getInstance().callJavaGet("any-id"), GlutenException);
}

} // namespace gluten
13 changes: 11 additions & 2 deletions docs/developers/MicroBenchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,7 @@ or 4 types of dumped file:
be more than one split file in a first stage task. Contains the substrait plan piece to the input
file splits.
- Data file(optional): Parquet formatted, file
name `data_{stageId}_{partitionId}_{vId}_{iteratorIdx}.parquet`. If the first stage contains one or
more BHJ operators, there can be one or more input data files. The input data files of a first
name `data_{stageId}_{partitionId}_{vId}_{iteratorIdx}.parquet`. The input data files of a first
stage will be loaded as iterators to serve as the inputs for the pipeline:

```
Expand All @@ -152,6 +151,16 @@ or 4 types of dumped file:
}
```

### Limitation: broadcast hash join

A stage containing a broadcast hash join cannot currently be replayed faithfully. The build side of
a BHJ is not streamed to the native operator: the hash table is built on the driver, broadcast in
serialized form, and registered in a process-local cache that the standalone benchmark has no access
to. The corresponding build side input iterator yields no rows, so its dumped
`data_{stageId}_{partitionId}_{vId}_{iteratorIdx}.parquet` is empty and replaying the stage produces
no join output. The benchmark logs a warning that no JVM is attached and that a new hash table will
be built from the (empty) build side input.

Run benchmark. By default, the result will be printed to stdout. You can use `--noprint-result` to
suppress this output.

Expand Down
Loading