/* * 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.processor.util.pattern; import org.apache.nifi.logging.ComponentLog; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.util.pattern.TestExceptionHandler.ExternalProcedure; import org.apache.nifi.util.MockComponentLog; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; import static org.apache.nifi.processor.util.pattern.TestExceptionHandler.createArrayInputErrorHandler; import static org.apache.nifi.processor.util.pattern.TestExceptionHandler.exceptionMapping; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; public class TestRollbackOnFailure { private static final Logger logger = LoggerFactory.getLogger(TestRollbackOnFailure.class); /** * This can be an example for how to compose an ExceptionHandler instance by reusable functions. * @param logger used to log messages within functions * @return a composed ExceptionHandler */ private ExceptionHandler<RollbackOnFailure> getContextAwareExceptionHandler(ComponentLog logger) { final ExceptionHandler<RollbackOnFailure> handler = new ExceptionHandler<>(); handler.mapException(exceptionMapping); handler.adjustError(RollbackOnFailure.createAdjustError(logger)); handler.onError(createArrayInputErrorHandler()); return handler; } private void processInputs(RollbackOnFailure context, Integer[][] inputs, List<Integer> results) { final ExternalProcedure p = new ExternalProcedure(); final MockComponentLog componentLog = new MockComponentLog("processor-id", this); final ExceptionHandler<RollbackOnFailure> handler = getContextAwareExceptionHandler(componentLog); for (Integer[] input : inputs) { if (!handler.execute(context, input, (in) -> { results.add(p.divide(in[0], in[1])); context.proceed(); })){ continue; } assertEquals(input[2], results.get(results.size() - 1)); } } @Test public void testContextDefaultBehavior() { // Disabling rollbackOnFailure would route Failure or Retry as they are. final RollbackOnFailure context = new RollbackOnFailure(false, false); Integer[][] inputs = new Integer[][]{{null, 2, 999}, {4, 2, 2}, {2, 0, 999}, {10, 2, 999}, {8, 2, 4}}; final List<Integer> results = new ArrayList<>(); try { processInputs(context, inputs, results); } catch (ProcessException e) { fail("ProcessException should NOT be thrown"); } assertEquals("Successful inputs", 2, context.getProcessedCount()); } @Test public void testContextRollbackOnFailureNonTransactionalFirstFailure() { final RollbackOnFailure context = new RollbackOnFailure(true, false); // If the first execution fails without any succeeded inputs, it should throw a ProcessException. Integer[][] inputs = new Integer[][]{{null, 2, 999}, {4, 2, 2}, {2, 0, 999}, {10, 2, 999}, {8, 2, 4}}; final List<Integer> results = new ArrayList<>(); try { processInputs(context, inputs, results); fail("ProcessException should be thrown"); } catch (ProcessException e) { logger.info("Exception was thrown as expected."); } assertEquals("Successful inputs", 0, context.getProcessedCount()); } @Test public void testContextRollbackOnFailureNonTransactionalAlreadySucceeded() { final RollbackOnFailure context = new RollbackOnFailure(true, false); // If an execution fails after succeeded inputs, it transfer the input to Failure instead of ProcessException, // and keep going. Because the external system does not support transaction. Integer[][] inputs = new Integer[][]{{4, 2, 2}, {2, 0, 999}, {null, 2, 999}, {10, 2, 999}, {8, 2, 4}}; final List<Integer> results = new ArrayList<>(); try { processInputs(context, inputs, results); } catch (ProcessException e) { fail("ProcessException should NOT be thrown"); } assertEquals("Successful inputs", 2, context.getProcessedCount()); } @Test public void testContextRollbackOnFailureTransactionalAlreadySucceeded() { final RollbackOnFailure context = new RollbackOnFailure(true, true); // Even if an execution fails after succeeded inputs, it transfer the input to Failure, // because the external system supports transaction. Integer[][] inputs = new Integer[][]{{4, 2, 2}, {2, 0, 999}, {null, 2, 999}, {10, 2, 999}, {8, 2, 4}}; final List<Integer> results = new ArrayList<>(); try { processInputs(context, inputs, results); fail("ProcessException should be thrown"); } catch (ProcessException e) { logger.info("Exception was thrown as expected."); } } }