This is an automated email from the ASF dual-hosted git repository.
davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 4c4b81859b [Fix][Connector-V2] Wait for Kudu client shutdown (#11585)
4c4b81859b is described below
commit 4c4b81859b090f9e8c724970c6995952de24bb4d
Author: zhiwei.niu <[email protected]>
AuthorDate: Wed Jul 29 23:51:59 2026 +0800
[Fix][Connector-V2] Wait for Kudu client shutdown (#11585)
---
.../seatunnel/kudu/catalog/KuduCatalog.java | 14 +++-
.../kudu/kuduclient/KuduClientResource.java | 82 ++++++++++++++++++++++
.../seatunnel/kudu/kuduclient/KuduInputFormat.java | 12 +++-
.../kudu/kuduclient/KuduOutputFormat.java | 32 +++++++--
.../seatunnel/kudu/source/KuduSourceReader.java | 22 +++---
.../kudu/source/KuduSourceSplitEnumerator.java | 35 +++++----
.../connectors/seatunnel/kudu/util/KuduUtil.java | 65 ++++++++++++++---
.../kudu/kuduclient/KuduClientResourceTest.java | 82 ++++++++++++++++++++++
8 files changed, 304 insertions(+), 40 deletions(-)
diff --git
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/catalog/KuduCatalog.java
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/catalog/KuduCatalog.java
index e03e223463..3d24cd480f 100644
---
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/catalog/KuduCatalog.java
+++
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/catalog/KuduCatalog.java
@@ -34,6 +34,7 @@ import
org.apache.seatunnel.api.table.catalog.exception.TableNotExistException;
import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
import org.apache.seatunnel.connectors.seatunnel.kudu.config.CommonConfig;
import org.apache.seatunnel.connectors.seatunnel.kudu.config.KuduBaseOptions;
+import
org.apache.seatunnel.connectors.seatunnel.kudu.kuduclient.KuduClientResource;
import
org.apache.seatunnel.connectors.seatunnel.kudu.kuduclient.KuduTypeMapper;
import org.apache.seatunnel.connectors.seatunnel.kudu.util.KuduUtil;
@@ -59,6 +60,7 @@ public class KuduCatalog implements Catalog {
private final CommonConfig config;
private KuduClient kuduClient;
+ private KuduClientResource kuduClientResource;
private final String defaultDatabase = "default_database";
@@ -71,15 +73,23 @@ public class KuduCatalog implements Catalog {
@Override
public void open() throws CatalogException {
- kuduClient = KuduUtil.getKuduClient(config);
+ kuduClientResource = KuduUtil.getKuduClientResource(config);
+ kuduClient = kuduClientResource.getClient();
}
@Override
public void close() throws CatalogException {
try {
- kuduClient.close();
+ if (kuduClientResource != null) {
+ kuduClientResource.close();
+ } else if (kuduClient != null) {
+ kuduClient.close();
+ }
} catch (KuduException e) {
throw new CatalogException("Failed close kudu client", e);
+ } finally {
+ kuduClient = null;
+ kuduClientResource = null;
}
}
diff --git
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduClientResource.java
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduClientResource.java
new file mode 100644
index 0000000000..0b07e8fc0f
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduClientResource.java
@@ -0,0 +1,82 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.kudu.kuduclient;
+
+import org.apache.kudu.client.KuduClient;
+import org.apache.kudu.client.KuduException;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Owns a Kudu client and the executor used by its Netty event loops.
+ *
+ * <p>Kudu 1.15 initiates a graceful Netty shutdown from {@link
KuduClient#close()} but returns
+ * before the event-loop threads terminate. Waiting for the supplied executor
prevents those threads
+ * from accessing connector classes after an engine unloads the user-code
classloader.
+ */
+@Slf4j
+public class KuduClientResource implements AutoCloseable {
+
+ private static final long GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS = 20L;
+ private static final long FORCED_SHUTDOWN_TIMEOUT_SECONDS = 5L;
+
+ private final KuduClient kuduClient;
+ private final ExecutorService executorService;
+
+ public KuduClientResource(KuduClient kuduClient, ExecutorService
executorService) {
+ this.kuduClient = kuduClient;
+ this.executorService = executorService;
+ }
+
+ public KuduClient getClient() {
+ return kuduClient;
+ }
+
+ @Override
+ public void close() throws KuduException {
+ try {
+ kuduClient.close();
+ } finally {
+ awaitExecutorTermination();
+ }
+ }
+
+ private void awaitExecutorTermination() {
+ executorService.shutdown();
+ try {
+ if (executorService.awaitTermination(
+ GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+ return;
+ }
+
+ log.warn("Kudu client executor did not terminate gracefully,
forcing shutdown.");
+ executorService.shutdownNow();
+ if (!executorService.awaitTermination(
+ FORCED_SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+ log.warn("Kudu client executor did not terminate after forced
shutdown.");
+ }
+ } catch (InterruptedException e) {
+ executorService.shutdownNow();
+ Thread.currentThread().interrupt();
+ log.warn("Interrupted while waiting for Kudu client executor to
terminate.", e);
+ }
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduInputFormat.java
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduInputFormat.java
index b34cda3dd9..90a41d85e2 100644
---
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduInputFormat.java
+++
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduInputFormat.java
@@ -56,13 +56,16 @@ public class KuduInputFormat implements Serializable {
/** Declare the global variable KuduClient and use it to manipulate the
Kudu table */
public KuduClient kuduClient;
+ private transient KuduClientResource kuduClientResource;
+
public KuduInputFormat(@NonNull KuduSourceConfig kuduSourceConfig) {
this.kuduSourceConfig = kuduSourceConfig;
}
public void openInputFormat() {
if (kuduClient == null) {
- kuduClient = KuduUtil.getKuduClient(kuduSourceConfig);
+ kuduClientResource =
KuduUtil.getKuduClientResource(kuduSourceConfig);
+ kuduClient = kuduClientResource.getClient();
}
}
@@ -84,12 +87,17 @@ public class KuduInputFormat implements Serializable {
public void closeInputFormat() {
if (kuduClient != null) {
try {
- kuduClient.close();
+ if (kuduClientResource != null) {
+ kuduClientResource.close();
+ } else {
+ kuduClient.close();
+ }
} catch (KuduException e) {
throw new KuduConnectorException(
KuduConnectorErrorCode.CLOSE_KUDU_CLIENT_FAILED, e);
} finally {
kuduClient = null;
+ kuduClientResource = null;
}
}
}
diff --git
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduOutputFormat.java
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduOutputFormat.java
index 01667c6e7d..4e9978291a 100644
---
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduOutputFormat.java
+++
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduOutputFormat.java
@@ -51,6 +51,7 @@ public class KuduOutputFormat implements Serializable {
private final KuduSinkConfig.SaveMode saveMode;
private final KuduSinkConfig kuduSinkConfig;
private KuduClient kuduClient;
+ private transient KuduClientResource kuduClientResource;
private KuduSession kuduSession;
private KuduTable kuduTable;
@@ -71,18 +72,36 @@ public class KuduOutputFormat implements Serializable {
}
private void openOutputFormat() {
- this.kuduClient = KuduUtil.getKuduClient(kuduSinkConfig);
- this.kuduSession = getSession();
try {
+ this.kuduClientResource =
KuduUtil.getKuduClientResource(kuduSinkConfig);
+ this.kuduClient = kuduClientResource.getClient();
+ this.kuduSession = getSession();
kuduTable = kuduClient.openTable(kuduTableName);
+ seaTunnelRowSerializer = new KuduRowSerializer(kuduTable,
saveMode, seaTunnelRowType);
} catch (KuduException e) {
+ closeClientAfterInitializationFailure(e);
throw new
KuduConnectorException(KuduConnectorErrorCode.INIT_KUDU_CLIENT_FAILED, e);
+ } catch (RuntimeException e) {
+ closeClientAfterInitializationFailure(e);
+ throw e;
}
log.info(
"The Kudu client for Master: {} is initialized successfully.",
kuduSinkConfig.getMasters());
+ }
- seaTunnelRowSerializer = new KuduRowSerializer(kuduTable, saveMode,
seaTunnelRowType);
+ private void closeClientAfterInitializationFailure(Exception
initializationException) {
+ if (kuduClientResource == null) {
+ return;
+ }
+ try {
+ kuduClientResource.close();
+ } catch (Exception closeException) {
+ initializationException.addSuppressed(closeException);
+ } finally {
+ kuduClient = null;
+ kuduClientResource = null;
+ }
}
private KuduSession getSession() {
@@ -108,11 +127,16 @@ public class KuduOutputFormat implements Serializable {
log.error("Error while closing session.", e);
}
try {
- if (kuduClient != null) {
+ if (kuduClientResource != null) {
+ kuduClientResource.close();
+ } else if (kuduClient != null) {
kuduClient.close();
}
} catch (Exception e) {
log.error("Error while closing client.", e);
+ } finally {
+ kuduClient = null;
+ kuduClientResource = null;
}
}
}
diff --git
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/source/KuduSourceReader.java
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/source/KuduSourceReader.java
index 5a83d55429..5871b9179b 100644
---
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/source/KuduSourceReader.java
+++
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/source/KuduSourceReader.java
@@ -84,14 +84,20 @@ public class KuduSourceReader implements
SourceReader<SeaTunnelRow, KuduSourceSp
TablePath tablePath = split.getTablePath();
SeaTunnelRowType seaTunnelRowType = tables.get(tablePath);
KuduScanner kuduScanner =
kuduInputFormat.scanner(split.getToken());
- while (kuduScanner.hasMoreRows()) {
- RowResultIterator rowResults = kuduScanner.nextRows();
- while (rowResults.hasNext()) {
- RowResult rowResult = rowResults.next();
- SeaTunnelRow seaTunnelRow =
- kuduInputFormat.toInternal(rowResult,
seaTunnelRowType);
- seaTunnelRow.setTableId(tablePath.toString());
- output.collect(seaTunnelRow);
+ try {
+ while (kuduScanner.hasMoreRows()) {
+ RowResultIterator rowResults = kuduScanner.nextRows();
+ while (rowResults.hasNext()) {
+ RowResult rowResult = rowResults.next();
+ SeaTunnelRow seaTunnelRow =
+ kuduInputFormat.toInternal(rowResult,
seaTunnelRowType);
+ seaTunnelRow.setTableId(tablePath.toString());
+ output.collect(seaTunnelRow);
+ }
+ }
+ } finally {
+ if (!kuduScanner.isClosed()) {
+ kuduScanner.close();
}
}
} else if (noMoreSplit && splits.isEmpty()) {
diff --git
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/source/KuduSourceSplitEnumerator.java
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/source/KuduSourceSplitEnumerator.java
index 6121e005a4..0d23c99beb 100644
---
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/source/KuduSourceSplitEnumerator.java
+++
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/source/KuduSourceSplitEnumerator.java
@@ -93,26 +93,33 @@ public class KuduSourceSplitEnumerator
@Override
public void run() throws IOException {
+ try {
+ Set<Integer> readers = enumeratorContext.registeredReaders();
+ while (!pendingTables.isEmpty()) {
+ synchronized (stateLock) {
+ TablePath tablePath = pendingTables.poll();
+ log.info("Splitting table {}.", tablePath);
- Set<Integer> readers = enumeratorContext.registeredReaders();
- while (!pendingTables.isEmpty()) {
- synchronized (stateLock) {
- TablePath tablePath = pendingTables.poll();
- log.info("Splitting table {}.", tablePath);
+ Collection<KuduSourceSplit> splits =
discoverySplits(tables.get(tablePath));
+ log.info("Split table {} into {} splits.", tablePath,
splits.size());
- Collection<KuduSourceSplit> splits =
discoverySplits(tables.get(tablePath));
- log.info("Split table {} into {} splits.", tablePath,
splits.size());
+ addPendingSplit(splits);
+ }
- addPendingSplit(splits);
+ synchronized (stateLock) {
+ assignSplit(readers);
+ }
}
- synchronized (stateLock) {
- assignSplit(readers);
- }
+ log.info(
+ "No more splits to assign." + " Sending NoMoreSplitsEvent
to reader {}.",
+ readers);
+ readers.forEach(enumeratorContext::signalNoMoreSplits);
+ } finally {
+ // Scan tokens contain everything readers need, so the enumerator
client can be
+ // released immediately instead of retaining Netty threads until
job teardown.
+ kuduInputFormat.closeInputFormat();
}
-
- log.info("No more splits to assign." + " Sending NoMoreSplitsEvent to
reader {}.", readers);
- readers.forEach(enumeratorContext::signalNoMoreSplits);
}
private Set<KuduSourceSplit> discoverySplits(KuduSourceTableConfig
kuduSourceTableConfig)
diff --git
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/util/KuduUtil.java
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/util/KuduUtil.java
index 1c7989f4da..3faded4a5e 100644
---
a/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/util/KuduUtil.java
+++
b/seatunnel-connectors-v2/connector-kudu/src/main/java/org/apache/seatunnel/connectors/seatunnel/kudu/util/KuduUtil.java
@@ -25,6 +25,7 @@ import
org.apache.seatunnel.connectors.seatunnel.kudu.config.KuduSourceConfig;
import
org.apache.seatunnel.connectors.seatunnel.kudu.config.KuduSourceTableConfig;
import
org.apache.seatunnel.connectors.seatunnel.kudu.exception.KuduConnectorErrorCode;
import
org.apache.seatunnel.connectors.seatunnel.kudu.exception.KuduConnectorException;
+import
org.apache.seatunnel.connectors.seatunnel.kudu.kuduclient.KuduClientResource;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.UserGroupInformation;
@@ -46,6 +47,9 @@ import java.io.IOException;
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -68,13 +72,50 @@ public class KuduUtil {
UserGroupInformation ugi = loginAndReturnUgi(config);
return ugi.doAs(
(PrivilegedExceptionAction<KuduClient>)
- () -> getKuduClientInternal(config));
+ () -> getKuduClientInternal(config, null));
}
}
- return getKuduClientInternal(config);
+ return getKuduClientInternal(config, null);
+ } catch (IOException | InterruptedException e) {
+ throw new
KuduConnectorException(KuduConnectorErrorCode.INIT_KUDU_CLIENT_FAILED, e);
+ }
+ }
+ public static KuduClientResource getKuduClientResource(CommonConfig
config) {
+ AtomicInteger threadCounter = new AtomicInteger();
+ ExecutorService executorService =
+ Executors.newCachedThreadPool(
+ runnable -> {
+ Thread thread =
+ new Thread(
+ runnable,
+ "seatunnel-kudu-nio-"
+ +
threadCounter.incrementAndGet());
+ thread.setDaemon(true);
+ return thread;
+ });
+ boolean initialized = false;
+ try {
+ KuduClient kuduClient;
+ if (config.getEnableKerberos()) {
+ synchronized (UserGroupInformation.class) {
+ UserGroupInformation ugi = loginAndReturnUgi(config);
+ kuduClient =
+ ugi.doAs(
+ (PrivilegedExceptionAction<KuduClient>)
+ () ->
getKuduClientInternal(config, executorService));
+ }
+ } else {
+ kuduClient = getKuduClientInternal(config, executorService);
+ }
+ initialized = true;
+ return new KuduClientResource(kuduClient, executorService);
} catch (IOException | InterruptedException e) {
throw new
KuduConnectorException(KuduConnectorErrorCode.INIT_KUDU_CLIENT_FAILED, e);
+ } finally {
+ if (!initialized) {
+ executorService.shutdownNow();
+ }
}
}
@@ -109,14 +150,18 @@ public class KuduUtil {
}
}
- private static KuduClient getKuduClientInternal(CommonConfig config) {
- return new AsyncKuduClient.AsyncKuduClientBuilder(
- Arrays.asList(config.getMasters().split(",")))
- .workerCount(config.getWorkerCount())
-
.defaultAdminOperationTimeoutMs(config.getAdminOperationTimeout())
- .defaultOperationTimeoutMs(config.getOperationTimeout())
- .build()
- .syncClient();
+ private static KuduClient getKuduClientInternal(
+ CommonConfig config, ExecutorService executorService) {
+ AsyncKuduClient.AsyncKuduClientBuilder builder =
+ new AsyncKuduClient.AsyncKuduClientBuilder(
+ Arrays.asList(config.getMasters().split(",")))
+ .workerCount(config.getWorkerCount())
+
.defaultAdminOperationTimeoutMs(config.getAdminOperationTimeout())
+
.defaultOperationTimeoutMs(config.getOperationTimeout());
+ if (executorService != null) {
+ builder.nioExecutor(executorService);
+ }
+ return builder.build().syncClient();
}
public static List<KuduScanToken> getKuduScanToken(
diff --git
a/seatunnel-connectors-v2/connector-kudu/src/test/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduClientResourceTest.java
b/seatunnel-connectors-v2/connector-kudu/src/test/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduClientResourceTest.java
new file mode 100644
index 0000000000..4b2cb2ad4e
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-kudu/src/test/java/org/apache/seatunnel/connectors/seatunnel/kudu/kuduclient/KuduClientResourceTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.kudu.kuduclient;
+
+import org.apache.kudu.client.KuduClient;
+import org.apache.kudu.client.KuduException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+class KuduClientResourceTest {
+
+ @Test
+ void shouldWaitForExecutorAfterClosingClient() throws Exception {
+ KuduClient kuduClient = Mockito.mock(KuduClient.class);
+ ExecutorService executorService = Mockito.mock(ExecutorService.class);
+ Mockito.when(executorService.awaitTermination(20L,
TimeUnit.SECONDS)).thenReturn(true);
+
+ new KuduClientResource(kuduClient, executorService).close();
+
+ InOrder inOrder = Mockito.inOrder(kuduClient, executorService);
+ inOrder.verify(kuduClient).close();
+ inOrder.verify(executorService).shutdown();
+ inOrder.verify(executorService).awaitTermination(20L,
TimeUnit.SECONDS);
+ Mockito.verify(executorService, Mockito.never()).shutdownNow();
+ }
+
+ @Test
+ void shouldForceShutdownWhenGracefulShutdownTimesOut() throws Exception {
+ KuduClient kuduClient = Mockito.mock(KuduClient.class);
+ ExecutorService executorService = Mockito.mock(ExecutorService.class);
+ Mockito.when(executorService.awaitTermination(20L,
TimeUnit.SECONDS)).thenReturn(false);
+ Mockito.when(executorService.awaitTermination(5L,
TimeUnit.SECONDS)).thenReturn(true);
+
+ new KuduClientResource(kuduClient, executorService).close();
+
+ InOrder inOrder = Mockito.inOrder(kuduClient, executorService);
+ inOrder.verify(kuduClient).close();
+ inOrder.verify(executorService).shutdown();
+ inOrder.verify(executorService).awaitTermination(20L,
TimeUnit.SECONDS);
+ inOrder.verify(executorService).shutdownNow();
+ inOrder.verify(executorService).awaitTermination(5L, TimeUnit.SECONDS);
+ }
+
+ @Test
+ void shouldShutdownExecutorWhenClosingClientFails() throws Exception {
+ KuduClient kuduClient = Mockito.mock(KuduClient.class);
+ KuduException closeException = Mockito.mock(KuduException.class);
+ Mockito.doThrow(closeException).when(kuduClient).close();
+ ExecutorService executorService = Mockito.mock(ExecutorService.class);
+ Mockito.when(executorService.awaitTermination(20L,
TimeUnit.SECONDS)).thenReturn(true);
+
+ KuduException actualException =
+ Assertions.assertThrows(
+ KuduException.class,
+ () -> new KuduClientResource(kuduClient,
executorService).close());
+
+ Assertions.assertSame(closeException, actualException);
+ Mockito.verify(executorService).shutdown();
+ Mockito.verify(executorService).awaitTermination(20L,
TimeUnit.SECONDS);
+ }
+}