/* * 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.nifi.controller.scheduling; import java.io.File; import org.apache.nifi.annotation.lifecycle.OnDisabled; import org.apache.nifi.annotation.lifecycle.OnEnabled; import org.apache.nifi.annotation.lifecycle.OnScheduled; import org.apache.nifi.bundle.Bundle; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.components.state.StateManagerProvider; import org.apache.nifi.controller.AbstractControllerService; import org.apache.nifi.controller.ConfigurationContext; import org.apache.nifi.controller.FlowController; import org.apache.nifi.controller.LoggableComponent; import org.apache.nifi.controller.ProcessScheduler; import org.apache.nifi.controller.ProcessorNode; import org.apache.nifi.controller.ReloadComponent; import org.apache.nifi.controller.ReportingTaskNode; import org.apache.nifi.controller.StandardProcessorNode; import org.apache.nifi.controller.ValidationContextFactory; import org.apache.nifi.controller.cluster.Heartbeater; import org.apache.nifi.controller.reporting.StandardReportingInitializationContext; import org.apache.nifi.controller.reporting.StandardReportingTaskNode; import org.apache.nifi.controller.service.ControllerServiceNode; import org.apache.nifi.controller.service.ControllerServiceProvider; import org.apache.nifi.controller.service.ControllerServiceState; import org.apache.nifi.controller.service.StandardControllerServiceNode; import org.apache.nifi.controller.service.StandardControllerServiceProvider; import org.apache.nifi.controller.service.mock.MockProcessGroup; import org.apache.nifi.groups.ProcessGroup; import org.apache.nifi.logging.ComponentLog; import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.nar.SystemBundle; import org.apache.nifi.processor.AbstractProcessor; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.Processor; import org.apache.nifi.processor.StandardProcessorInitializationContext; import org.apache.nifi.processor.StandardValidationContextFactory; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.registry.VariableRegistry; import org.apache.nifi.reporting.AbstractReportingTask; import org.apache.nifi.reporting.InitializationException; import org.apache.nifi.reporting.ReportingContext; import org.apache.nifi.reporting.ReportingInitializationContext; import org.apache.nifi.reporting.ReportingTask; import org.apache.nifi.scheduling.SchedulingStrategy; import org.apache.nifi.util.NiFiProperties; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.io.FileUtils; import org.junit.After; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class TestStandardProcessScheduler { private StandardProcessScheduler scheduler = null; private ReportingTaskNode taskNode = null; private TestReportingTask reportingTask = null; private final StateManagerProvider stateMgrProvider = Mockito.mock(StateManagerProvider.class); private VariableRegistry variableRegistry = VariableRegistry.ENVIRONMENT_SYSTEM_REGISTRY; private FlowController controller; private ProcessGroup rootGroup; private NiFiProperties nifiProperties; private Bundle systemBundle; private volatile String propsFile = TestStandardProcessScheduler.class.getResource("/standardprocessschedulertest.nifi.properties").getFile(); @Before public void setup() throws InitializationException { this.nifiProperties = NiFiProperties.createBasicNiFiProperties(propsFile, null); // load the system bundle systemBundle = SystemBundle.create(nifiProperties); ExtensionManager.discoverExtensions(systemBundle, Collections.emptySet()); scheduler = new StandardProcessScheduler(Mockito.mock(ControllerServiceProvider.class), null, stateMgrProvider, variableRegistry, nifiProperties); scheduler.setSchedulingAgent(SchedulingStrategy.TIMER_DRIVEN, Mockito.mock(SchedulingAgent.class)); reportingTask = new TestReportingTask(); final ReportingInitializationContext config = new StandardReportingInitializationContext(UUID.randomUUID().toString(), "Test", SchedulingStrategy.TIMER_DRIVEN, "5 secs", Mockito.mock(ComponentLog.class), null, nifiProperties); reportingTask.initialize(config); final ValidationContextFactory validationContextFactory = new StandardValidationContextFactory(null, variableRegistry); final ComponentLog logger = Mockito.mock(ComponentLog.class); final ReloadComponent reloadComponent = Mockito.mock(ReloadComponent.class); final LoggableComponent<ReportingTask> loggableComponent = new LoggableComponent<>(reportingTask, systemBundle.getBundleDetails().getCoordinate(), logger); taskNode = new StandardReportingTaskNode(loggableComponent, UUID.randomUUID().toString(), null, scheduler, validationContextFactory, variableRegistry, reloadComponent); controller = Mockito.mock(FlowController.class); rootGroup = new MockProcessGroup(); Mockito.when(controller.getGroup(Mockito.anyString())).thenReturn(rootGroup); } @After public void after() throws Exception { controller.shutdown(true); FileUtils.deleteDirectory(new File("./target/standardprocessschedulertest")); } /** * We have run into an issue where a Reporting Task is scheduled to run but * throws an Exception from a method with the @OnScheduled annotation. User * stops Reporting Task, updates configuration to fix the issue. Reporting * Task then finishes running @OnSchedule method and is then scheduled to * run. This unit test is intended to verify that we have this resolved. */ @Test public void testReportingTaskDoesntKeepRunningAfterStop() throws InterruptedException, InitializationException { scheduler.schedule(taskNode); // Let it try to run a few times. Thread.sleep(1000L); scheduler.unschedule(taskNode); final int attempts = reportingTask.onScheduleAttempts.get(); // give it a sec to make sure that it's finished running. Thread.sleep(1500L); final int attemptsAfterStop = reportingTask.onScheduleAttempts.get() - attempts; // allow 1 extra run, due to timing issues that could call it as it's being stopped. assertTrue("After unscheduling Reporting Task, task ran an additional " + attemptsAfterStop + " times", attemptsAfterStop <= 1); } @Test(timeout = 60000) public void testDisableControllerServiceWithProcessorTryingToStartUsingIt() throws InterruptedException { final String uuid = UUID.randomUUID().toString(); final Processor proc = new ServiceReferencingProcessor(); proc.initialize(new StandardProcessorInitializationContext(uuid, null, null, null, null)); final ReloadComponent reloadComponent = Mockito.mock(ReloadComponent.class); final StandardControllerServiceProvider serviceProvider = new StandardControllerServiceProvider(controller, scheduler, null, Mockito.mock(StateManagerProvider.class), variableRegistry, nifiProperties); final ControllerServiceNode service = serviceProvider.createControllerService(NoStartServiceImpl.class.getName(), "service", systemBundle.getBundleDetails().getCoordinate(), null, true); rootGroup.addControllerService(service); final LoggableComponent<Processor> loggableComponent = new LoggableComponent<>(proc, systemBundle.getBundleDetails().getCoordinate(), null); final ProcessorNode procNode = new StandardProcessorNode(loggableComponent, uuid, new StandardValidationContextFactory(serviceProvider, variableRegistry), scheduler, serviceProvider, nifiProperties, VariableRegistry.EMPTY_REGISTRY, reloadComponent); rootGroup.addProcessor(procNode); Map<String, String> procProps = new HashMap<>(); procProps.put(ServiceReferencingProcessor.SERVICE_DESC.getName(), service.getIdentifier()); procNode.setProperties(procProps); scheduler.enableControllerService(service); scheduler.startProcessor(procNode); Thread.sleep(1000L); scheduler.stopProcessor(procNode); assertTrue(service.isActive()); assertTrue(service.getState() == ControllerServiceState.ENABLING); scheduler.disableControllerService(service); assertTrue(service.getState() == ControllerServiceState.DISABLING); assertFalse(service.isActive()); Thread.sleep(2000); assertTrue(service.getState() == ControllerServiceState.DISABLED); } private class TestReportingTask extends AbstractReportingTask { private final AtomicBoolean failOnScheduled = new AtomicBoolean(true); private final AtomicInteger onScheduleAttempts = new AtomicInteger(0); private final AtomicInteger triggerCount = new AtomicInteger(0); @OnScheduled public void onScheduled() { onScheduleAttempts.incrementAndGet(); if (failOnScheduled.get()) { throw new RuntimeException("Intentional Exception for testing purposes"); } } @Override public void onTrigger(final ReportingContext context) { triggerCount.getAndIncrement(); } } private static class ServiceReferencingProcessor extends AbstractProcessor { static final PropertyDescriptor SERVICE_DESC = new PropertyDescriptor.Builder() .name("service") .identifiesControllerService(NoStartService.class) .required(true) .build(); @Override protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { final List<PropertyDescriptor> properties = new ArrayList<>(); properties.add(SERVICE_DESC); return properties; } @Override public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { } } /** * Validates the atomic nature of ControllerServiceNode.enable() method * which must only trigger @OnEnabled once, regardless of how many threads * may have a reference to the underlying ProcessScheduler and * ControllerServiceNode. */ @Test public void validateServiceEnablementLogicHappensOnlyOnce() throws Exception { final ProcessScheduler scheduler = createScheduler(); final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties); final ControllerServiceNode serviceNode = provider.createControllerService(SimpleTestService.class.getName(), "1", systemBundle.getBundleDetails().getCoordinate(), null, false); assertFalse(serviceNode.isActive()); final SimpleTestService ts = (SimpleTestService) serviceNode.getControllerServiceImplementation(); final ExecutorService executor = Executors.newCachedThreadPool(); final AtomicBoolean asyncFailed = new AtomicBoolean(); for (int i = 0; i < 1000; i++) { executor.execute(new Runnable() { @Override public void run() { try { scheduler.enableControllerService(serviceNode); assertTrue(serviceNode.isActive()); } catch (final Exception e) { e.printStackTrace(); asyncFailed.set(true); } } }); } // need to sleep a while since we are emulating async invocations on // method that is also internally async Thread.sleep(500); executor.shutdown(); assertFalse(asyncFailed.get()); assertEquals(1, ts.enableInvocationCount()); } /** * Validates the atomic nature of ControllerServiceNode.disable(..) method * which must never trigger @OnDisabled, regardless of how many threads may * have a reference to the underlying ProcessScheduler and * ControllerServiceNode. */ @Test public void validateDisabledServiceCantBeDisabled() throws Exception { final ProcessScheduler scheduler = createScheduler(); final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties); final ControllerServiceNode serviceNode = provider.createControllerService(SimpleTestService.class.getName(), "1", systemBundle.getBundleDetails().getCoordinate(), null, false); final SimpleTestService ts = (SimpleTestService) serviceNode.getControllerServiceImplementation(); final ExecutorService executor = Executors.newCachedThreadPool(); final AtomicBoolean asyncFailed = new AtomicBoolean(); for (int i = 0; i < 1000; i++) { executor.execute(new Runnable() { @Override public void run() { try { scheduler.disableControllerService(serviceNode); assertFalse(serviceNode.isActive()); } catch (final Exception e) { e.printStackTrace(); asyncFailed.set(true); } } }); } // need to sleep a while since we are emulating async invocations on // method that is also internally async Thread.sleep(500); executor.shutdown(); assertFalse(asyncFailed.get()); assertEquals(0, ts.disableInvocationCount()); } /** * Validates the atomic nature of ControllerServiceNode.disable() method * which must only trigger @OnDisabled once, regardless of how many threads * may have a reference to the underlying ProcessScheduler and * ControllerServiceNode. */ @Test public void validateEnabledServiceCanOnlyBeDisabledOnce() throws Exception { final ProcessScheduler scheduler = createScheduler(); final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties); final ControllerServiceNode serviceNode = provider.createControllerService(SimpleTestService.class.getName(), "1", systemBundle.getBundleDetails().getCoordinate(), null, false); final SimpleTestService ts = (SimpleTestService) serviceNode.getControllerServiceImplementation(); scheduler.enableControllerService(serviceNode); assertTrue(serviceNode.isActive()); final ExecutorService executor = Executors.newCachedThreadPool(); final AtomicBoolean asyncFailed = new AtomicBoolean(); for (int i = 0; i < 1000; i++) { executor.execute(new Runnable() { @Override public void run() { try { scheduler.disableControllerService(serviceNode); assertFalse(serviceNode.isActive()); } catch (final Exception e) { e.printStackTrace(); asyncFailed.set(true); } } }); } // need to sleep a while since we are emulating async invocations on // method that is also internally async Thread.sleep(500); executor.shutdown(); assertFalse(asyncFailed.get()); assertEquals(1, ts.disableInvocationCount()); } @Test public void validateDisablingOfTheFailedService() throws Exception { final ProcessScheduler scheduler = createScheduler(); final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties); final ControllerServiceNode serviceNode = provider.createControllerService(FailingService.class.getName(), "1", systemBundle.getBundleDetails().getCoordinate(), null, false); scheduler.enableControllerService(serviceNode); Thread.sleep(1000); scheduler.shutdown(); /* * Because it was never disabled it will remain active since its * enabling is being retried. This may actually be a bug in the * scheduler since it probably has to shut down all components (disable * services, shut down processors etc) before shutting down itself */ assertTrue(serviceNode.isActive()); assertTrue(serviceNode.getState() == ControllerServiceState.ENABLING); } /** * Validates that in multi threaded environment enabling service can still * be disabled. This test is set up in such way that disabling of the * service could be initiated by both disable and enable methods. In other * words it tests two conditions in * {@link StandardControllerServiceNode#disable(java.util.concurrent.ScheduledExecutorService, Heartbeater)} * where the disabling of the service can be initiated right there (if * ENABLED), or if service is still enabling its disabling will be deferred * to the logic in * {@link StandardControllerServiceNode#enable(java.util.concurrent.ScheduledExecutorService, long, Heartbeater)} * IN any even the resulting state of the service is DISABLED */ @Test @Ignore public void validateEnabledDisableMultiThread() throws Exception { final ProcessScheduler scheduler = createScheduler(); final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties); final ExecutorService executor = Executors.newCachedThreadPool(); for (int i = 0; i < 200; i++) { final ControllerServiceNode serviceNode = provider.createControllerService(RandomShortDelayEnablingService.class.getName(), "1", systemBundle.getBundleDetails().getCoordinate(), null, false); executor.execute(new Runnable() { @Override public void run() { scheduler.enableControllerService(serviceNode); } }); Thread.sleep(10); // ensure that enable gets initiated before disable executor.execute(new Runnable() { @Override public void run() { scheduler.disableControllerService(serviceNode); } }); Thread.sleep(100); assertFalse(serviceNode.isActive()); assertTrue(serviceNode.getState() == ControllerServiceState.DISABLED); } // need to sleep a while since we are emulating async invocations on // method that is also internally async Thread.sleep(500); executor.shutdown(); executor.awaitTermination(5000, TimeUnit.MILLISECONDS); } /** * Validates that service that is infinitely blocking in @OnEnabled can * still have DISABLE operation initiated. The service itself will be set to * DISABLING state at which point UI and all will know that such service can * not be transitioned any more into any other state until it finishes * enabling (which will never happen in our case thus should be addressed by * user). However, regardless of user's mistake NiFi will remain * functioning. */ @Test public void validateNeverEnablingServiceCanStillBeDisabled() throws Exception { final ProcessScheduler scheduler = createScheduler(); final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties); final ControllerServiceNode serviceNode = provider.createControllerService(LongEnablingService.class.getName(), "1", systemBundle.getBundleDetails().getCoordinate(), null, false); final LongEnablingService ts = (LongEnablingService) serviceNode.getControllerServiceImplementation(); ts.setLimit(Long.MAX_VALUE); scheduler.enableControllerService(serviceNode); Thread.sleep(100); assertTrue(serviceNode.isActive()); assertEquals(1, ts.enableInvocationCount()); Thread.sleep(1000); scheduler.disableControllerService(serviceNode); assertFalse(serviceNode.isActive()); assertEquals(ControllerServiceState.DISABLING, serviceNode.getState()); assertEquals(0, ts.disableInvocationCount()); } /** * Validates that the service that is currently in ENABLING state can be * disabled and that its @OnDisabled operation will be invoked as soon as * * @OnEnable finishes. */ @Test public void validateLongEnablingServiceCanStillBeDisabled() throws Exception { final ProcessScheduler scheduler = createScheduler(); final StandardControllerServiceProvider provider = new StandardControllerServiceProvider(controller, scheduler, null, stateMgrProvider, variableRegistry, nifiProperties); final ControllerServiceNode serviceNode = provider.createControllerService(LongEnablingService.class.getName(), "1", systemBundle.getBundleDetails().getCoordinate(), null, false); final LongEnablingService ts = (LongEnablingService) serviceNode.getControllerServiceImplementation(); ts.setLimit(3000); scheduler.enableControllerService(serviceNode); Thread.sleep(2000); assertTrue(serviceNode.isActive()); assertEquals(1, ts.enableInvocationCount()); Thread.sleep(500); scheduler.disableControllerService(serviceNode); assertFalse(serviceNode.isActive()); assertEquals(ControllerServiceState.DISABLING, serviceNode.getState()); assertEquals(0, ts.disableInvocationCount()); // wait a bit. . . Enabling will finish and @OnDisabled will be invoked // automatically Thread.sleep(4000); assertEquals(ControllerServiceState.DISABLED, serviceNode.getState()); assertEquals(1, ts.disableInvocationCount()); } public static class FailingService extends AbstractControllerService { @OnEnabled public void enable(final ConfigurationContext context) { throw new RuntimeException("intentional"); } } public static class RandomShortDelayEnablingService extends AbstractControllerService { private final Random random = new Random(); @OnEnabled public void enable(final ConfigurationContext context) { try { Thread.sleep(random.nextInt(20)); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); } } } public static class SimpleTestService extends AbstractControllerService { private final AtomicInteger enableCounter = new AtomicInteger(); private final AtomicInteger disableCounter = new AtomicInteger(); @OnEnabled public void enable(final ConfigurationContext context) { this.enableCounter.incrementAndGet(); } @OnDisabled public void disable(final ConfigurationContext context) { this.disableCounter.incrementAndGet(); } public int enableInvocationCount() { return this.enableCounter.get(); } public int disableInvocationCount() { return this.disableCounter.get(); } } public static class LongEnablingService extends AbstractControllerService { private final AtomicInteger enableCounter = new AtomicInteger(); private final AtomicInteger disableCounter = new AtomicInteger(); private volatile long limit; @OnEnabled public void enable(final ConfigurationContext context) throws Exception { this.enableCounter.incrementAndGet(); Thread.sleep(limit); } @OnDisabled public void disable(final ConfigurationContext context) { this.disableCounter.incrementAndGet(); } public int enableInvocationCount() { return this.enableCounter.get(); } public int disableInvocationCount() { return this.disableCounter.get(); } public void setLimit(final long limit) { this.limit = limit; } } private ProcessScheduler createScheduler() { return new StandardProcessScheduler(null, null, stateMgrProvider, variableRegistry, nifiProperties); } }