content: /*
   * 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.kafka.connect.runtime;
  
  import org.apache.kafka.common.utils.MockTime;
  import org.apache.kafka.common.utils.Time;
  import org.apache.kafka.connect.errors.ConnectException;
  import org.apache.kafka.connect.runtime.TaskStatus.Listener;
  import org.apache.kafka.connect.runtime.WorkerTask.TaskMetricsGroup;
  import org.apache.kafka.connect.runtime.errors.ErrorHandlingMetrics;
  import org.apache.kafka.connect.runtime.errors.ErrorReporter;
  import org.apache.kafka.connect.runtime.errors.RetryWithToleranceOperator;
  import org.apache.kafka.connect.runtime.isolation.TestPlugins;
  import org.apache.kafka.connect.sink.SinkTask;
  import org.apache.kafka.connect.source.SourceRecord;
  import org.apache.kafka.connect.storage.StatusBackingStore;
  import org.apache.kafka.connect.util.ConnectorTaskId;
  
  import org.junit.jupiter.api.AfterEach;
  import org.junit.jupiter.api.BeforeEach;
  import org.junit.jupiter.api.Test;
  import org.junit.jupiter.api.extension.ExtendWith;
  import org.mockito.Mock;
  import org.mockito.junit.jupiter.MockitoExtension;
  import org.mockito.junit.jupiter.MockitoSettings;
  import org.mockito.quality.Strictness;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.CountDownLatch;
  import java.util.function.Supplier;
  
  import static org.junit.jupiter.api.Assertions.assertEquals;
  import static org.junit.jupiter.api.Assertions.assertThrows;
  import static org.junit.jupiter.api.Assertions.fail;
  import static org.mockito.Mockito.times;
  import static org.mockito.Mockito.verify;
  import static org.mockito.Mockito.verifyNoMoreInteractions;
  import static org.mockito.Mockito.when;
  
  @ExtendWith(MockitoExtension.class)
  @MockitoSettings(strictness = Strictness.STRICT_STUBS)
  public class WorkerTaskTest {
  
      private static final Map<String, String> TASK_PROPS = new HashMap<>();
      static {
          TASK_PROPS.put(TaskConfig.TASK_CLASS_CONFIG, TestSinkTask.class.getName());
      }
      private static final TaskConfig TASK_CONFIG = new TaskConfig(TASK_PROPS);
  
      @Mock private TaskStatus.Listener statusListener;
      @Mock private ClassLoader loader;
      @Mock private StatusBackingStore statusBackingStore;
      private ConnectMetrics metrics;
      @Mock private ErrorHandlingMetrics errorHandlingMetrics;
      @Mock private RetryWithToleranceOperator<Object> retryWithToleranceOperator;
      @Mock private TransformationChain<Object, SourceRecord> transformationChain;
      @Mock private Supplier<List<ErrorReporter<Object>>> errorReportersSupplier;
  
      @BeforeEach
      public void setup() {
          metrics = new MockConnectMetrics();
      }
  
      @AfterEach
      public void tearDown() {
          if (metrics != null) metrics.stop();
      }
  
      @Test
      public void standardStartup() {
          ConnectorTaskId taskId = new ConnectorTaskId("foo", 0);
  
          WorkerTask<Object, SourceRecord> workerTask = new TestWorkerTask(taskId, statusListener, TargetState.STARTED, loader, metrics, errorHandlingMetrics,
                  retryWithToleranceOperator, transformationChain, errorReportersSupplier, Time.SYSTEM, statusBackingStore);
          workerTask.initialize(TASK_CONFIG);
          workerTask.run();
          workerTask.stop();
          workerTask.awaitStop(1000L);
  
          verify(statusListener).onStartup(taskId);
          verify(statusListener).onShutdown(taskId);
      }
  
      @Test
      public void stopBeforeStarting() {
          ConnectorTaskId taskId = new ConnectorTaskId("foo", 0);
  
          WorkerTask<Object, SourceRecord> workerTask = new TestWorkerTask(taskId, statusListener, TargetState.STARTED, loader, metrics, errorHandlingMetrics,
                  retryWithToleranceOperator, transformationChain, errorReportersSupplier, Time.SYSTEM, statusBackingStore) {
  
              @Override
              public void initializeAndStart() {
                  fail("This method is expected to not be invoked");
              }
  
              @Override
              public void execute() {
                  fail("This method is expected to not be invoked");
              }
          };
  
          workerTask.initialize(TASK_CONFIG);
          workerTask.stop();
          workerTask.awaitStop(1000L);
  
          // now run should not do anything
          workerTask.run();
      }
  
      @Test
      public void cancelBeforeStopping() throws Exception {
          ConnectorTaskId taskId = new ConnectorTaskId("foo", 0);
          final CountDownLatch stopped = new CountDownLatch(1);
  
          WorkerTask<Object, SourceRecord> workerTask = new TestWorkerTask(taskId, statusListener, TargetState.STARTED, loader, metrics, errorHandlingMetrics,
                  retryWithToleranceOperator, transformationChain, errorReportersSupplier, Time.SYSTEM, statusBackingStore) {
  
              @Override
              public void execute() {
                  try {
                      stopped.await();
                  } catch (InterruptedException e) {
                      fail("Unexpected interrupt");
                  }
              }
  
              // Trigger task shutdown immediately after start. The task will block in its execute() method
              // until the stopped latch is counted down (i.e. it doesn't actually stop after stop is triggered).
              @Override
              public void initializeAndStart() {
                  stop();
              }
          };
  
          workerTask.initialize(TASK_CONFIG);
          Thread t = new Thread(workerTask);
          t.start();
  
          workerTask.cancel();
          stopped.countDown();
          t.join();
  
          verify(statusListener).onStartup(taskId);
          // there should be no other status updates, including shutdown
          verifyNoMoreInteractions(statusListener);
      }
  
      @Test
      public void testErrorReportersConfigured() {
          ConnectorTaskId taskId = new ConnectorTaskId("foo", 0);
  
          WorkerTask<Object, SourceRecord> workerTask = new TestWorkerTask(taskId, statusListener, TargetState.STARTED, loader, metrics, errorHandlingMetrics,
                  retryWithToleranceOperator, transformationChain, errorReportersSupplier, Time.SYSTEM, statusBackingStore);
  
          List<ErrorReporter<Object>> errorReporters = new ArrayList<>();
          when(errorReportersSupplier.get()).thenReturn(errorReporters);
  
          workerTask.doStart();
          verify(retryWithToleranceOperator).reporters(errorReporters);
      }
  
      @Test
      public void testErrorReporterConfigurationExceptionPropagation() {
          ConnectorTaskId taskId = new ConnectorTaskId("foo", 0);
  
          WorkerTask<Object, SourceRecord> workerTask = new TestWorkerTask(taskId, statusListener, TargetState.STARTED, loader, metrics, errorHandlingMetrics,
                  retryWithToleranceOperator, transformationChain, errorReportersSupplier, Time.SYSTEM, statusBackingStore);
          when(errorReportersSupplier.get()).thenThrow(new ConnectException("Failed to create error reporters"));
  
          assertThrows(ConnectException.class, workerTask::doStart);
      }
  
      @Test
      public void testCloseClosesManagedResources() {
          ConnectorTaskId taskId = new ConnectorTaskId("foo", 0);
  
          WorkerTask<Object, SourceRecord> workerTask = new TestWorkerTask(taskId, statusListener, TargetState.STARTED, loader, metrics, errorHandlingMetrics,
                  retryWithToleranceOperator, transformationChain, errorReportersSupplier, Time.SYSTEM, statusBackingStore);
  
          workerTask.doClose();
  
          verify(retryWithToleranceOperator).close();
          verify(transformationChain).close();
      }
  
      @Test
      public void testCloseClosesManagedResourcesIfSubclassThrows() {
          ConnectorTaskId taskId = new ConnectorTaskId("foo", 0);
  
          WorkerTask<Object, SourceRecord> workerTask = new TestWorkerTask(taskId, statusListener, TargetState.STARTED, loader, metrics, errorHandlingMetrics,
                  retryWithToleranceOperator, transformationChain, errorReportersSupplier, Time.SYSTEM, statusBackingStore) {
              @Override
              protected void close() {
                  throw new ConnectException("Failure during close");
              }
          };
  
          assertThrows(ConnectException.class, workerTask::doClose);
  
          verify(retryWithToleranceOperator).close();
          verify(transformationChain).close();
      }
  
      @Test
      public void updateMetricsOnListenerEventsForStartupPauseResumeAndShutdown() {
          ConnectorTaskId taskId = new ConnectorTaskId("foo", 0);
          ConnectMetrics metrics = new MockConnectMetrics();
          TaskMetricsGroup group = new TaskMetricsGroup(taskId, metrics, statusListener);
  
          group.onStartup(taskId);
          assertRunningMetric(group);
          group.onPause(taskId);
          assertPausedMetric(group);
          group.onResume(taskId);
          assertRunningMetric(group);
          group.onShutdown(taskId);
          assertStoppedMetric(group);
  
          verify(statusListener).onStartup(taskId);
          verify(statusListener).onPause(taskId);
          verify(statusListener).onResume(taskId);
          verify(statusListener).onShutdown(taskId);
      }
  
      @Test
      public void updateMetricsOnListenerEventsForStartupPauseResumeAndFailure() {
          ConnectorTaskId taskId = new ConnectorTaskId("foo", 0);
          MockConnectMetrics metrics = new MockConnectMetrics();
          MockTime time = metrics.time();
          ConnectException error = new ConnectException("error");
          TaskMetricsGroup group = new TaskMetricsGroup(taskId, metrics, statusListener);
  
          time.sleep(1000L);
          group.onStartup(taskId);
          assertRunningMetric(group);
  
          time.sleep(2000L);
          group.onPause(taskId);
          assertPausedMetric(group);
  
          time.sleep(3000L);
          group.onResume(taskId);
          assertRunningMetric(group);
  
          time.sleep(4000L);
          group.onPause(taskId);
          assertPausedMetric(group);
  
          time.sleep(5000L);
          group.onResume(taskId);
          assertRunningMetric(group);
  
          time.sleep(6000L);
          group.onFailure(taskId, error);
          assertFailedMetric(group);
  
          time.sleep(7000L);
          group.onShutdown(taskId);
          assertStoppedMetric(group);
  
          verify(statusListener).onStartup(taskId);
          verify(statusListener, times(2)).onPause(taskId);
          verify(statusListener, times(2)).onResume(taskId);
          verify(statusListener).onFailure(taskId, error);
          verify(statusListener).onShutdown(taskId);
  
          long totalTime = 27000L;
          double pauseTimeRatio = (double) (3000L + 5000L) / totalTime;
          double runningTimeRatio = (double) (2000L + 4000L + 6000L) / totalTime;
          assertEquals(pauseTimeRatio, metrics.currentMetricValueAsDouble(group.metricGroup(), "pause-ratio"), 0.000001d);
          assertEquals(runningTimeRatio, metrics.currentMetricValueAsDouble(group.metricGroup(), "running-ratio"), 0.000001d);
      }
  
      private abstract static class TestSinkTask extends SinkTask {
      }
  
      private static class TestWorkerTask extends WorkerTask<Object, SourceRecord> {
  
          public TestWorkerTask(ConnectorTaskId id, Listener statusListener, TargetState initialState, ClassLoader loader,
                                ConnectMetrics connectMetrics, ErrorHandlingMetrics errorHandlingMetrics,
                                RetryWithToleranceOperator<Object> retryWithToleranceOperator,
                                TransformationChain<Object, SourceRecord> transformationChain,
                                Supplier<List<ErrorReporter<Object>>> errorReporterSupplier,
                                Time time, StatusBackingStore statusBackingStore) {
              super(id, statusListener, initialState, loader, connectMetrics, errorHandlingMetrics,
                      retryWithToleranceOperator, transformationChain, errorReporterSupplier, time, statusBackingStore, null, TestPlugins.noOpLoaderSwap());
          }
  
          @Override
          public void initialize(TaskConfig taskConfig) {
          }
  
          @Override
          protected void initializeAndStart() {
          }
  
          @Override
          protected void execute() {
          }
  
          @Override
          protected void close() {
          }
  
          @Override
          protected String taskVersion() {
              return null;
          }
      }
  
      protected void assertFailedMetric(TaskMetricsGroup metricsGroup) {
          assertEquals(AbstractStatus.State.FAILED, metricsGroup.state());
      }
  
      protected void assertPausedMetric(TaskMetricsGroup metricsGroup) {
          assertEquals(AbstractStatus.State.PAUSED, metricsGroup.state());
      }
  
      protected void assertRunningMetric(TaskMetricsGroup metricsGroup) {
          assertEquals(AbstractStatus.State.RUNNING, metricsGroup.state());
      }
  
      protected void assertStoppedMetric(TaskMetricsGroup metricsGroup) {
          assertEquals(AbstractStatus.State.UNASSIGNED, metricsGroup.state());
      }
  }
  