This is an automated email from the ASF dual-hosted git repository.
danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new 75678093b3eb fix(lock): fix lock config derivation for Spark procedure
(#19794)
75678093b3eb is described below
commit 75678093b3eb6af46b8dcfa1c1e41ccc384a9afd
Author: Joy <[email protected]>
AuthorDate: Tue Sep 8 16:20:32 2026 +0800
fix(lock): fix lock config derivation for Spark procedure (#19794)
* fix(lock): unify default FS lock path and make CLI lock config take effect
Two related defects in the metadata-table auto-lock path:
1. Lock-path inconsistency: FileSystemBasedLockProvider.getLockConfig used
the .aux folder as the default lock path, while the constructor fell back
to the .hoodie meta folder. The two defaults could diverge, silently
placing the lock file at different paths and breaking mutual exclusion
across engines/tasks. Unify both on the table metadata path (.hoodie) by
reusing a single defaultLockPath() helper.
2. Lock config never applied: RunClustering/RunCompaction appended lock
options to `confs` AFTER the write client was already built, so they had
no effect. Move the auto-lock injection down into
HoodieCLIUtils.createHoodieWriteClient, applied after the final
parameters
are merged and before the client is built, guarded on the fully-merged
params so any explicitly-configured lock provider is respected. This also
extends the coverage from just clustering/compaction to all write
procedures that go through createHoodieWriteClient.
Remove the now-dead lock-injection blocks and unused imports from the two
procedures. Add/extend tests covering the unified path and getLockOptions.
* fix(lock): keep metadata lock derivation in clustering and compaction
---------
Co-authored-by: jiangyu84 <[email protected]>
Co-authored-by: danny0405 <[email protected]>
---
.../lock/FileSystemBasedLockProvider.java | 5 +-
.../lock/TestFileSystemBasedLockProvider.java | 38 +++++++++--
.../scala/org/apache/hudi/HoodieCLIUtils.scala | 34 +++++++---
.../scala/org/apache/hudi/TestHoodieCLIUtils.scala | 72 +++++++++++++++++++++
.../procedures/RunClusteringProcedure.scala | 13 ++--
.../procedures/RunCompactionProcedure.scala | 14 ++--
.../procedure/TestTableServiceLockConfig.scala | 74 ++++++++++++++++++++++
7 files changed, 222 insertions(+), 28 deletions(-)
diff --git
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/FileSystemBasedLockProvider.java
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/FileSystemBasedLockProvider.java
index 0988cf4a7054..da3279e32013 100644
---
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/FileSystemBasedLockProvider.java
+++
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/FileSystemBasedLockProvider.java
@@ -24,7 +24,6 @@ import org.apache.hudi.common.config.LockConfiguration;
import org.apache.hudi.common.config.TypedProperties;
import org.apache.hudi.common.lock.LockProvider;
import org.apache.hudi.common.lock.LockState;
-import org.apache.hudi.common.table.HoodieTableMetaClient;
import org.apache.hudi.common.util.HoodieStorageUtils;
import org.apache.hudi.common.util.StringUtils;
import org.apache.hudi.common.util.ValidationUtils;
@@ -115,8 +114,8 @@ public class FileSystemBasedLockProvider implements
LockProvider<String>, Serial
this.lockConfiguration = lockConfiguration;
String lockDirectory =
lockConfiguration.getConfig().getString(FILESYSTEM_LOCK_PATH_PROP_KEY, null);
if (StringUtils.isNullOrEmpty(lockDirectory)) {
- lockDirectory =
lockConfiguration.getConfig().getString(HoodieWriteConfig.BASE_PATH.key())
- + StoragePath.SEPARATOR + HoodieTableMetaClient.METAFOLDER_NAME;
+ // Match getLockConfig() so writers using either default share the same
lock.
+ lockDirectory =
defaultLockPath(lockConfiguration.getConfig().getString(HoodieWriteConfig.BASE_PATH.key()));
}
this.lockTimeoutMinutes =
lockConfiguration.getConfig().getInteger(FILESYSTEM_LOCK_EXPIRE_PROP_KEY);
this.lockFile = new StoragePath(lockDirectory + StoragePath.SEPARATOR +
LOCK_FILE_NAME);
diff --git
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/TestFileSystemBasedLockProvider.java
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/TestFileSystemBasedLockProvider.java
index e2ca7a8a02cd..1fd1eaa69071 100644
---
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/TestFileSystemBasedLockProvider.java
+++
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/TestFileSystemBasedLockProvider.java
@@ -433,8 +433,8 @@ public class TestFileSystemBasedLockProvider {
public void testGetLockConfigProducesUsableProperties() {
String tablePath = tempDir.resolve("table").toString();
TypedProperties props =
FileSystemBasedLockProvider.getLockConfig(tablePath);
- // The generated config points the lock provider at the table's auxiliary
folder.
-
assertTrue(props.getString(HoodieLockConfig.FILESYSTEM_LOCK_PATH.key()).startsWith(tablePath));
+ assertEquals(tablePath + StoragePath.SEPARATOR +
HoodieTableMetaClient.AUXILIARYFOLDER_NAME,
+ props.getString(HoodieLockConfig.FILESYSTEM_LOCK_PATH.key()));
assertEquals(FileSystemBasedLockProvider.class.getName(),
props.getString(HoodieLockConfig.LOCK_PROVIDER_CLASS_NAME.key()));
@@ -450,7 +450,35 @@ public class TestFileSystemBasedLockProvider {
}
@Test
- public void testLockPathDefaultsToMetafolderFromBasePath() {
+ public void testExplicitLockConfigAndBasePathFallbackResolveToSamePath() {
+ // Regression guard for the cross-engine lock-path divergence:
getLockConfig()'s explicit default
+ // and the constructor's BASE_PATH-derived fallback must point at the very
same lock file, or a
+ // writer relying on one would not be mutually excluded from a writer
relying on the other.
+ StorageConfiguration<?> storageConf =
HoodieTestUtils.getDefaultStorageConf();
+ String tablePath = tempDir.resolve("sharedtable").toString();
+
+ // 1) Provider built from getLockConfig() (explicit FILESYSTEM_LOCK_PATH).
+ FileSystemBasedLockProvider fromLockConfig =
+ new FileSystemBasedLockProvider(
+ new
LockConfiguration(FileSystemBasedLockProvider.getLockConfig(tablePath)),
storageConf);
+
+ // 2) Provider built with only BASE_PATH set, exercising the constructor
fallback.
+ Properties fallbackProps = new Properties();
+ fallbackProps.setProperty(HoodieWriteConfig.BASE_PATH.key(), tablePath);
+ fallbackProps.setProperty(FILESYSTEM_LOCK_EXPIRE_PROP_KEY, "0");
+ FileSystemBasedLockProvider fromFallback =
+ new FileSystemBasedLockProvider(new LockConfiguration(fallbackProps),
storageConf);
+
+ // getLock() is a pure accessor of the resolved lock-file path (no storage
I/O), so comparing it
+ // is enough and needs no acquire/release.
+ assertEquals(fromLockConfig.getLock(), fromFallback.getLock(),
+ "explicit default and BASE_PATH fallback must resolve to the same lock
file");
+ assertTrue(fromLockConfig.getLock()
+ .endsWith(HoodieTableMetaClient.AUXILIARYFOLDER_NAME +
StoragePath.SEPARATOR + "lock"));
+ }
+
+ @Test
+ public void testLockPathDefaultsToAuxiliaryFolderFromBasePath() {
StorageConfiguration<?> storageConf =
HoodieTestUtils.getDefaultStorageConf();
Properties props = new Properties();
props.setProperty(HoodieWriteConfig.BASE_PATH.key(),
lockDir("defaultpath"));
@@ -460,9 +488,9 @@ public class TestFileSystemBasedLockProvider {
try {
assertTrue(provider.tryLock(1, TimeUnit.SECONDS),
"lock acquisition must work without an explicit lock path");
- // Without an explicit lock path the provider locks under the table
metafolder.
+ // Without an explicit lock path the provider locks under the table
auxiliary folder.
assertTrue(provider.getLock().endsWith(
- HoodieTableMetaClient.METAFOLDER_NAME + StoragePath.SEPARATOR +
"lock"));
+ HoodieTableMetaClient.AUXILIARYFOLDER_NAME + StoragePath.SEPARATOR +
"lock"));
} finally {
provider.unlock();
provider.close();
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieCLIUtils.scala
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieCLIUtils.scala
index c6e37f5bb6dd..c46a0595899d 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieCLIUtils.scala
+++
b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieCLIUtils.scala
@@ -37,7 +37,7 @@ import
org.apache.spark.sql.catalyst.catalog.HoodieCatalogTable
import org.apache.spark.sql.hudi.HoodieOptionConfig
import org.apache.spark.sql.hudi.HoodieSqlCommonUtils.filterHoodieConfigs
-import java.util.ArrayList
+import java.util.Collections
import scala.collection.JavaConverters.{collectionAsScalaIterableConverter,
mapAsJavaMapConverter, propertiesAsScalaMapConverter}
@@ -52,6 +52,16 @@ object HoodieCLIUtils extends Logging {
val schemaUtil = new TableSchemaResolver(metaClient)
val schemaStr = schemaUtil.getTableSchema(false).toString
+ val finalParameters = getWriteParameters(sparkSession, metaClient, conf,
tableName)
+ val jsc = new JavaSparkContext(sparkSession.sparkContext)
+ DataSourceUtils.createHoodieClient(jsc, schemaStr, basePath,
+ metaClient.getTableConfig.getTableName, finalParameters.asJava)
+ }
+
+ def getWriteParameters(sparkSession: SparkSession,
+ metaClient: HoodieTableMetaClient,
+ conf: Map[String, String],
+ tableName: Option[String]): Map[String, String] = {
// If tableName is provided, we need to add catalog props
val catalogProps = tableName match {
case Some(value) =>
HoodieOptionConfig.mapSqlOptionsToDataSourceWriteConfigs(
@@ -60,16 +70,12 @@ object HoodieCLIUtils extends Logging {
}
// Priority: defaults < catalog props < table config < sparkSession conf <
specified conf
- val finalParameters = HoodieWriterUtils.parametersWithWriteDefaults(
+ HoodieWriterUtils.parametersWithWriteDefaults(
(catalogProps ++
metaClient.getTableConfig.getProps.asScala.toMap ++
filterHoodieConfigs(sparkSession.sqlContext.getAllConfs) ++
conf).toMap
)
-
- val jsc = new JavaSparkContext(sparkSession.sparkContext)
- DataSourceUtils.createHoodieClient(jsc, schemaStr, basePath,
- metaClient.getTableConfig.getTableName, finalParameters.asJava)
}
def extractPartitions(clusteringGroups: Seq[HoodieClusteringGroup]): String
= {
@@ -166,9 +172,19 @@ object HoodieCLIUtils extends Logging {
key -> value
}
- def getLockOptions(tablePath: String, schema: String, lockConfig:
TypedProperties): Map[String, String] = {
- val customSupportedFSs =
lockConfig.getStringList(HoodieCommonConfig.HOODIE_FS_ATOMIC_CREATION_SUPPORT.key,
",", new ArrayList[String])
- if (schema == null || customSupportedFSs.contains(schema) ||
StorageSchemes.isAtomicCreationSupported(schema)) {
+ /**
+ * Builds the filesystem-based lock configuration for the metadata table, or
an empty map when the
+ * table's filesystem cannot support atomic file creation (a hard
requirement for the FS lock).
+ *
+ * @param tablePath the table base path, used to derive the shared lock file
location
+ * @param scheme the table filesystem scheme; {@code null} is treated as
supported
+ * @param params the already-merged write parameters, read only for the
custom
+ * atomic-creation-support list ({@code
hoodie.fs.atomic_creation.support})
+ */
+ def getLockOptions(tablePath: String, scheme: String, params: Map[String,
String]): Map[String, String] = {
+ val customSupportedFSs = TypedProperties.fromMap(params.asJava)
+ .getStringList(HoodieCommonConfig.HOODIE_FS_ATOMIC_CREATION_SUPPORT.key,
",", Collections.emptyList[String]())
+ if (scheme == null || customSupportedFSs.contains(scheme) ||
StorageSchemes.isAtomicCreationSupported(scheme)) {
logInfo("Auto config filesystem lock provider for metadata table")
val props = FileSystemBasedLockProvider.getLockConfig(tablePath)
props.stringPropertyNames.asScala
diff --git
a/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/hudi/TestHoodieCLIUtils.scala
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/hudi/TestHoodieCLIUtils.scala
index ca4869286c26..2818a0bce190 100644
---
a/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/hudi/TestHoodieCLIUtils.scala
+++
b/hudi-spark-datasource/hudi-spark-common/src/test/scala/org/apache/hudi/TestHoodieCLIUtils.scala
@@ -19,8 +19,15 @@
package org.apache.hudi
+import org.apache.hudi.client.transaction.lock.FileSystemBasedLockProvider
+import org.apache.hudi.common.config.{HoodieCommonConfig, TypedProperties}
+import org.apache.hudi.common.table.{HoodieTableConfig, HoodieTableMetaClient}
+import org.apache.hudi.config.HoodieLockConfig
+
+import org.apache.spark.sql.{SparkSession, SQLContext}
import org.junit.jupiter.api.Assertions.{assertEquals, assertThrows,
assertTrue}
import org.junit.jupiter.api.Test
+import org.mockito.Mockito.{mock, when}
class TestHoodieCLIUtils {
@@ -101,4 +108,69 @@ class TestHoodieCLIUtils {
classOf[IllegalArgumentException],
() => HoodieCLIUtils.extractOptions(" =v"))
}
+
+ @Test
+ def testGetLockOptionsSupportedSchemeReturnsFsLockConfig(): Unit = {
+ val tablePath = "/tmp/hudi/some_table"
+ // A null scheme is treated as supported; the FS lock provider must be
auto-configured.
+ val opts = HoodieCLIUtils.getLockOptions(tablePath, null, Map.empty)
+ assertEquals(classOf[FileSystemBasedLockProvider].getName,
+ opts(HoodieLockConfig.LOCK_PROVIDER_CLASS_NAME.key))
+ assertEquals(s"$tablePath/${HoodieTableMetaClient.AUXILIARYFOLDER_NAME}",
+ opts(HoodieLockConfig.FILESYSTEM_LOCK_PATH.key))
+ }
+
+ @Test
+ def testGetLockOptionsUnsupportedSchemeReturnsEmpty(): Unit = {
+ // s3 is a known scheme without atomic-creation support, so no FS lock can
be configured.
+ assertTrue(HoodieCLIUtils.getLockOptions("s3://bucket/table", "s3",
Map.empty).isEmpty)
+ }
+
+ @Test
+ def testGetLockOptionsCustomAtomicSupportEnablesScheme(): Unit = {
+ // Opting s3 into hoodie.fs.atomic_creation.support makes the FS lock
provider eligible again.
+ val params = Map(HoodieCommonConfig.HOODIE_FS_ATOMIC_CREATION_SUPPORT.key
-> " hdfs, s3 ")
+ val opts = HoodieCLIUtils.getLockOptions("s3://bucket/table", "s3", params)
+ assertEquals(classOf[FileSystemBasedLockProvider].getName,
+ opts(HoodieLockConfig.LOCK_PROVIDER_CLASS_NAME.key))
+ }
+
+ @Test
+ def testWriteParametersPreserveLockConfigPrecedence(): Unit = {
+ val sparkSession = mock(classOf[SparkSession])
+ val sqlContext = mock(classOf[SQLContext])
+ val metaClient = mock(classOf[HoodieTableMetaClient])
+ val tableConfig = mock(classOf[HoodieTableConfig])
+ val tableProps = new TypedProperties()
+ val providerKey = HoodieLockConfig.LOCK_PROVIDER_CLASS_NAME.key
+ val atomicSupportKey =
HoodieCommonConfig.HOODIE_FS_ATOMIC_CREATION_SUPPORT.key
+ tableProps.setProperty(providerKey, "table.provider")
+ tableProps.setProperty(atomicSupportKey, "hdfs")
+ when(sparkSession.sqlContext).thenReturn(sqlContext)
+ when(metaClient.getTableConfig).thenReturn(tableConfig)
+ when(tableConfig.getProps).thenReturn(tableProps)
+ when(sqlContext.getAllConfs).thenReturn(Map.empty[String, String])
+
+ val fromTable = HoodieCLIUtils.getWriteParameters(sparkSession,
metaClient, Map.empty, None)
+ assertEquals("table.provider", fromTable(providerKey))
+ assertEquals("hdfs", fromTable(atomicSupportKey))
+
+ when(sqlContext.getAllConfs).thenReturn(Map(providerKey ->
"session.provider", atomicSupportKey -> "s3"))
+ val fromSession = HoodieCLIUtils.getWriteParameters(sparkSession,
metaClient, Map.empty, None)
+ assertEquals("session.provider", fromSession(providerKey))
+ assertEquals("s3", fromSession(atomicSupportKey))
+
+ val fromOptions = HoodieCLIUtils.getWriteParameters(sparkSession,
metaClient,
+ Map(providerKey -> "options.provider", atomicSupportKey -> "file"), None)
+ assertEquals("options.provider", fromOptions(providerKey))
+ assertEquals("file", fromOptions(atomicSupportKey))
+ assertEquals(fromOptions,
+ HoodieCLIUtils.getWriteParameters(sparkSession, metaClient, fromOptions,
None))
+
+ tableProps.clear()
+ when(sqlContext.getAllConfs).thenReturn(Map.empty[String, String])
+ val withoutLock = HoodieCLIUtils.getWriteParameters(sparkSession,
metaClient, Map.empty, None)
+ assertTrue(!withoutLock.contains(providerKey))
+ }
+
}
diff --git
a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RunClusteringProcedure.scala
b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RunClusteringProcedure.scala
index e3248f337ffc..10c4c7b5ad70 100644
---
a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RunClusteringProcedure.scala
+++
b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RunClusteringProcedure.scala
@@ -181,6 +181,14 @@ class RunClusteringProcedure extends BaseProcedure
var (filteredPendingClusteringInstants, operation) =
HoodieProcedureUtils.filterPendingInstantsAndGetOperation(
pendingClusteringInstants,
specificInstants.asInstanceOf[Option[String]], op.asInstanceOf[Option[String]],
limit.asInstanceOf[Option[Int]])
+ confs = HoodieCLIUtils.getWriteParameters(sparkSession, metaClient, confs,
+ tableName.asInstanceOf[Option[String]])
+ // Apply the lock options before constructing the write client.
+ if (metaClient.getTableConfig.isMetadataTableAvailable
+ && !confs.contains(HoodieLockConfig.LOCK_PROVIDER_CLASS_NAME.key)) {
+ confs = HoodieCLIUtils.getLockOptions(basePath,
metaClient.getBasePath.toUri.getScheme, confs) ++ confs
+ }
+
var client: SparkRDDWriteClient[_] = null
try {
client = HoodieCLIUtils.createHoodieWriteClient(sparkSession, basePath,
confs,
@@ -197,11 +205,6 @@ class RunClusteringProcedure extends BaseProcedure
SpatialCurveSortPartitionerBase.validateOrderByColumns(orderColumns,
tableSchema, strategy)
}
}
- if (metaClient.getTableConfig.isMetadataTableAvailable) {
- if (!confs.contains(HoodieLockConfig.LOCK_PROVIDER_CLASS_NAME.key)) {
- confs = confs ++ HoodieCLIUtils.getLockOptions(basePath,
metaClient.getBasePath.toUri.getScheme,
client.getConfig.getCommonConfig.getProps())
- }
- }
if (operation.isSchedule) {
val instantTime = client.scheduleClustering(HOption.empty())
instantTime.ifPresent(instant => {
diff --git
a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RunCompactionProcedure.scala
b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RunCompactionProcedure.scala
index 772d8be1b5b7..f709b85a331b 100644
---
a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RunCompactionProcedure.scala
+++
b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RunCompactionProcedure.scala
@@ -94,17 +94,19 @@ class RunCompactionProcedure extends BaseProcedure with
ProcedureBuilder with Sp
var (filteredPendingCompactionInstants, operation) =
HoodieProcedureUtils.filterPendingInstantsAndGetOperation(
pendingCompactionInstants,
specificInstants.asInstanceOf[Option[String]], Option(op), limit)
+ confs = HoodieCLIUtils.getWriteParameters(sparkSession, metaClient, confs,
+ tableName.asInstanceOf[Option[String]])
+ // Apply the lock options before constructing the write client.
+ if (metaClient.getTableConfig.isMetadataTableAvailable
+ && !confs.contains(HoodieLockConfig.LOCK_PROVIDER_CLASS_NAME.key)) {
+ confs = HoodieCLIUtils.getLockOptions(basePath,
metaClient.getBasePath.toUri.getScheme, confs) ++ confs
+ }
+
var client: SparkRDDWriteClient[_] = null
try {
client = HoodieCLIUtils.createHoodieWriteClient(sparkSession, basePath,
confs,
tableName.asInstanceOf[Option[String]])
- if (metaClient.getTableConfig.isMetadataTableAvailable) {
- if (!confs.contains(HoodieLockConfig.LOCK_PROVIDER_CLASS_NAME.key)) {
- confs = confs ++ HoodieCLIUtils.getLockOptions(basePath,
metaClient.getBasePath.toUri.getScheme,
client.getConfig.getCommonConfig.getProps())
- }
- }
-
if (operation.isSchedule) {
val instantTime =
client.scheduleCompaction(HOption.empty[java.util.Map[String, String]])
instantTime.ifPresent(instant => {
diff --git
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestTableServiceLockConfig.scala
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestTableServiceLockConfig.scala
new file mode 100644
index 000000000000..ddcabdde533e
--- /dev/null
+++
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestTableServiceLockConfig.scala
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+
+package org.apache.spark.sql.hudi.procedure
+
+import org.apache.hudi.HoodieCLIUtils
+import org.apache.hudi.client.transaction.lock.FileSystemBasedLockProvider
+import org.apache.hudi.common.config.LockConfiguration
+import org.apache.hudi.common.testutils.HoodieTestUtils
+import org.apache.hudi.config.HoodieLockConfig
+import org.apache.hudi.exception.HoodieLockException
+
+import java.util.concurrent.TimeUnit
+
+class TestTableServiceLockConfig extends HoodieSparkProcedureTestBase {
+ Seq("run_clustering" -> "cow", "run_compaction" -> "mor").foreach { case
(procedure, tableType) =>
+ test(s"$procedure acquires the derived metadata table lock") {
+ withTempDir { tmp =>
+ val tableName = generateTableName
+ val basePath = tmp.getCanonicalPath
+ spark.sql(
+ s"""create table $tableName (id int, ts long) using hudi
+ |location '$basePath'
+ |tblproperties (primaryKey = 'id', preCombineField = 'ts', type =
'$tableType',
+ | 'hoodie.metadata.enable' = 'true')""".stripMargin)
+ spark.sql(s"insert into $tableName values (1, 1)")
+
assert(HoodieTestUtils.createMetaClient(basePath).getTableConfig.isMetadataTableAvailable)
+
+ val client = HoodieCLIUtils.createHoodieWriteClient(spark, basePath,
Map.empty, Some(tableName))
+ try {
+ assert(client.getConfig.getLockProviderClass !=
classOf[FileSystemBasedLockProvider].getName)
+ } finally {
+ client.close()
+ }
+
+ val lock = new FileSystemBasedLockProvider(
+ new
LockConfiguration(FileSystemBasedLockProvider.getLockConfig(basePath)),
+ HoodieTestUtils.getDefaultStorageConf)
+ try {
+ assert(lock.tryLock(1, TimeUnit.SECONDS))
+ val options = Seq(
+ HoodieLockConfig.LOCK_ACQUIRE_WAIT_TIMEOUT_MS.key -> "1",
+ HoodieLockConfig.LOCK_ACQUIRE_CLIENT_NUM_RETRIES.key -> "0"
+ ).map { case (key, value) => s"$key=$value" }.mkString(",")
+ val error = intercept[Exception] {
+ spark.sql(s"call $procedure(op => 'schedule', table =>
'$tableName', options => '$options')").collect()
+ }
+ assert(Iterator.iterate[Throwable](error)(_.getCause).takeWhile(_ !=
null)
+ .exists(_.isInstanceOf[HoodieLockException]))
+ } finally {
+ lock.unlock()
+ lock.close()
+ }
+ spark.sql(s"call $procedure(op => 'schedule', table =>
'$tableName')").collect()
+ }
+ }
+ }
+}