Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions engine/src/main/java/org/apache/hop/pipeline/PipelineMeta.java
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,46 @@ public void lookupReferencesAfterLoading() {
// This is rarely used, for example in getTableFields.getTableFields()
transformMeta.setParentPipelineMeta(this);
}
syncTransformErrorHandlingWithHops();
}

/**
* Align {@link TransformErrorMeta} with the enabled state of the hop to the error target.
* Pipelines saved before error hops were flagged in hop metadata can have error handling marked
* enabled while the hop to the target transform is disabled.
*/
public void syncTransformErrorHandlingWithHops() {
for (TransformMeta transformMeta : transforms) {
TransformErrorMeta errorMeta = transformMeta.getTransformErrorMeta();
if (errorMeta == null || errorMeta.getTargetTransform() == null) {
continue;
}
PipelineHopMeta hop = findPipelineHop(transformMeta, errorMeta.getTargetTransform(), true);
if (hop != null) {
errorMeta.setEnabled(hop.isEnabled());
}
applyDefaultErrorHandlingFieldNamesIfUnset(errorMeta);
}
}

/**
* Legacy pipelines often enable error handling without naming the extra error columns, so
* rejected rows carry no description field for downstream transforms (e.g. Write to log).
*/
private static void applyDefaultErrorHandlingFieldNamesIfUnset(TransformErrorMeta errorMeta) {
if (!errorMeta.isEnabled()) {
return;
}
if (!Utils.isEmpty(errorMeta.getNrErrorsValueName())
|| !Utils.isEmpty(errorMeta.getErrorDescriptionsValueName())
|| !Utils.isEmpty(errorMeta.getErrorFieldsValueName())
|| !Utils.isEmpty(errorMeta.getErrorCodesValueName())) {
return;
}
// error nr, error code, error description
errorMeta.setNrErrorsValueName(TransformErrorMeta.FIELD_ERROR_ROW);
errorMeta.setErrorCodesValueName(TransformErrorMeta.FIELD_ERROR_CODE);
errorMeta.setErrorDescriptionsValueName(TransformErrorMeta.FIELD_ERROR_DESCRIPTION);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
Expand Down Expand Up @@ -1520,6 +1521,19 @@ public void handlePutError(
}
}
incrementLinesRejected();
if (!Utils.isEmpty(errorDescriptions)) {
logError(errorDescriptions);
}
} else if (transformErrorMeta.isEnabled()) {
String name =
Objects.nonNull(transformErrorMeta.getTargetTransform())
? transformErrorMeta.getTargetTransform().getName()
: "targetTransform is null";
logError(
BaseMessages.getString(
PKG, "BaseTransform.Exception.ErrorHandlingRouteUnavailable", name));
setErrors(1);
stopAll();
}

verifyRejectionRates();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ public class TransformErrorMeta extends ChangedFlag implements Cloneable {
public static final String XML_SOURCE_TRANSFORM_TAG = "source_transform";
public static final String XML_TARGET_TRANSFORM_TAG = "target_transform";

/** error nr, error code, error description */
public static final String FIELD_ERROR_ROW = "error_row";

public static final String FIELD_ERROR_DESCRIPTION = "error_description";
public static final String FIELD_ERROR_CODE = "error_code";

/** The source transform that can send the error rows */
@HopMetadataProperty(key = "source_transform", storeWithName = true, lookupInList = "transforms")
private TransformMeta sourceTransform;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -616,10 +616,18 @@ public boolean supportsErrorHandling() {
* @return true if error handling is defined and enabled
*/
public boolean isDoingErrorHandling() {
return transform.supportsErrorHandling()
&& transformErrorMeta != null
&& transformErrorMeta.getTargetTransform() != null
&& transformErrorMeta.isEnabled();
if (!transform.supportsErrorHandling()
|| transformErrorMeta == null
|| transformErrorMeta.getTargetTransform() == null
|| !transformErrorMeta.isEnabled()) {
return false;
}

if (parentPipelineMeta != null) {
return parentPipelineMeta.findPipelineHop(this, transformErrorMeta.getTargetTransform())
!= null;
}
return true;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ BaseTransform.Exception.SourceTransformToReadFromCantRunInMultipleCopies=The sou
BaseTransform.Exception.SourceTransformToReadFromDoesntExist=The source transform to read from [{0}] couldn''t be found.
BaseTransform.Exception.TargetTransformToWriteToCantRunInMultipleCopies=The target transform [{0}] to write to can''t be run in multiple ({1}) copies.
BaseTransform.Exception.TargetTransformToWriteToDoesntExist=The target transform [{0}] to write to doesn''t exist.
BaseTransform.Exception.ErrorHandlingRouteUnavailable=Error handling is enabled for target transform [{0}] but no enabled hop routes errors to it. Re-enable the error handling hop or disable error handling on this transform.
BaseTransform.Log.FinishedDispatching=Finished dispatching
BaseTransform.Log.FoundInputRowset=Found input rowset [{0}]
BaseTransform.Log.FoundOutputRowset=Found output rowset [{0}]
Expand Down
66 changes: 66 additions & 0 deletions engine/src/test/java/org/apache/hop/pipeline/PipelineMetaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/** Unit test for {@link PipelineMeta} */
class PipelineMetaTest {
public static final String TRANSFORM_NAME = "Any transform name";

Expand Down Expand Up @@ -690,6 +691,71 @@ void testErrorHandlingSerialization() throws Exception {
assertNotNull(copy.findTransform("T1").getTransformErrorMeta());
}

@Test
void syncTransformErrorHandlingWithHopsAlignsErrorMetaWithHopEnabledState() {
TransformMeta source = new TransformMeta("REST client", new FakeMeta());
TransformMeta errorTarget = new TransformMeta("Write to log error", new FakeMeta());
pipelineMeta.addTransform(source);
pipelineMeta.addTransform(errorTarget);

PipelineHopMeta errorHop = new PipelineHopMeta(source, errorTarget);
errorHop.setEnabled(false);
pipelineMeta.addPipelineHop(errorHop);

TransformErrorMeta errorMeta = new TransformErrorMeta(source, errorTarget);
errorMeta.setEnabled(true);
source.setTransformErrorMeta(errorMeta);

pipelineMeta.lookupReferencesAfterLoading();

assertFalse(errorMeta.isEnabled());
assertFalse(source.isDoingErrorHandling());
}

@Test
void lookupReferencesAfterLoadingKeepsErrorHandlingActiveWhenHopEnabled() {
TransformMeta source = new TransformMeta("REST client", new FakeMeta());
TransformMeta errorTarget = new TransformMeta("Write to log error", new FakeMeta());
pipelineMeta.addTransform(source);
pipelineMeta.addTransform(errorTarget);

PipelineHopMeta errorHop = new PipelineHopMeta(source, errorTarget);
errorHop.setEnabled(true);
pipelineMeta.addPipelineHop(errorHop);

TransformErrorMeta errorMeta = new TransformErrorMeta(source, errorTarget);
errorMeta.setEnabled(true);
source.setTransformErrorMeta(errorMeta);

pipelineMeta.lookupReferencesAfterLoading();

assertTrue(errorMeta.isEnabled());
assertTrue(source.isDoingErrorHandling());
}

@Test
void syncTransformErrorHandlingAppliesDefaultErrorFieldNamesForLegacyPipelines() {
TransformMeta source = new TransformMeta("REST client", new FakeMeta());
TransformMeta errorTarget = new TransformMeta("Write to log error", new FakeMeta());
pipelineMeta.addTransform(source);
pipelineMeta.addTransform(errorTarget);

PipelineHopMeta errorHop = new PipelineHopMeta(source, errorTarget);
errorHop.setEnabled(true);
pipelineMeta.addPipelineHop(errorHop);

TransformErrorMeta errorMeta = new TransformErrorMeta(source, errorTarget);
errorMeta.setEnabled(true);
source.setTransformErrorMeta(errorMeta);

pipelineMeta.syncTransformErrorHandlingWithHops();

assertEquals(TransformErrorMeta.FIELD_ERROR_ROW, errorMeta.getNrErrorsValueName());
assertEquals(
TransformErrorMeta.FIELD_ERROR_DESCRIPTION, errorMeta.getErrorDescriptionsValueName());
assertEquals(TransformErrorMeta.FIELD_ERROR_CODE, errorMeta.getErrorCodesValueName());
}

@Test
void testPartitioningSerialization() throws Exception {
IHopMetadataSerializer<PartitionSchema> schemaSerializer =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,12 @@
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;

/** Unit test for {@link BaseTransform} */
@ExtendWith(MockitoExtension.class)
class BaseTransformTest {
private TransformMockHelper<ITransformMeta, ITransformData> mockHelper;

@Mock IRowHandler rowHandler;
@Mock private IRowHandler rowHandler;

@BeforeEach
void setup() {
Expand Down Expand Up @@ -302,6 +303,32 @@ void putErrorWithRowHandler() throws HopException {
.putError(iRowMeta, objects, 3L, "desc", "field1,field2", "errorCode");
}

@Test
void putErrorStopsPipelineWhenErrorRowSetUnavailable() throws HopException {
TransformMeta targetMeta = mock(TransformMeta.class);
when(targetMeta.getName()).thenReturn("Write to log error");
TransformErrorMeta errorMeta = new TransformErrorMeta(mockHelper.transformMeta, targetMeta);
errorMeta.setEnabled(true);
when(mockHelper.transformMeta.getTransformErrorMeta()).thenReturn(errorMeta);

BaseTransform<ITransformMeta, ITransformData> base =
spy(
new BaseTransform<>(
mockHelper.transformMeta,
mockHelper.iTransformMeta,
mockHelper.iTransformData,
0,
mockHelper.pipelineMeta,
mockHelper.pipeline));

IRowMeta iRowMeta = new RowMeta();
Object[] objects = new Object[] {"Bob", "Col"};
base.putError(iRowMeta, objects, 1L, "PKIX path building failed", null, "Rest001");

verify(base).stopAll();
assertTrue(base.getErrors() > 0);
}

@Test
void putGetFromPutToDefaultRowHandlerMethods() throws HopException {
BaseTransform<ITransformMeta, ITransformData> baseTransform =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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.hop.pipeline.transform;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
import org.apache.hop.pipeline.PipelineHopMeta;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.transform.transforms.FakeMeta;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

/** Tests for error-handling metadata vs hop enabled state (issue #7303). */
@ExtendWith(RestoreHopEngineEnvironmentExtension.class)
class TransformMetaErrorHandlingTest {

@Test
void isDoingErrorHandlingFalseWhenErrorHopDisabled() {
PipelineMeta pipelineMeta = new PipelineMeta();
TransformMeta source = new TransformMeta("REST client", new FakeMeta());
TransformMeta errorTarget = new TransformMeta("Write to log error", new FakeMeta());
pipelineMeta.addTransform(source);
pipelineMeta.addTransform(errorTarget);

PipelineHopMeta errorHop = new PipelineHopMeta(source, errorTarget);
errorHop.setEnabled(false);
pipelineMeta.addPipelineHop(errorHop);

TransformErrorMeta errorMeta = new TransformErrorMeta(source, errorTarget);
errorMeta.setEnabled(true);
source.setTransformErrorMeta(errorMeta);
source.setParentPipelineMeta(pipelineMeta);

assertFalse(source.isDoingErrorHandling());
}

@Test
void isDoingErrorHandlingTrueWhenErrorHopEnabled() {
PipelineMeta pipelineMeta = new PipelineMeta();
TransformMeta source = new TransformMeta("REST client", new FakeMeta());
TransformMeta errorTarget = new TransformMeta("Write to log error", new FakeMeta());
pipelineMeta.addTransform(source);
pipelineMeta.addTransform(errorTarget);

PipelineHopMeta errorHop = new PipelineHopMeta(source, errorTarget);
errorHop.setEnabled(true);
pipelineMeta.addPipelineHop(errorHop);

TransformErrorMeta errorMeta = new TransformErrorMeta(source, errorTarget);
errorMeta.setEnabled(true);
source.setTransformErrorMeta(errorMeta);
source.setParentPipelineMeta(pipelineMeta);

assertTrue(source.isDoingErrorHandling());
}

@Test
void isDoingErrorHandlingUsesErrorMetaWhenNoParentPipeline() {
TransformMeta source = new TransformMeta("REST client", new FakeMeta());
TransformMeta errorTarget = new TransformMeta("Write to log error", new FakeMeta());

TransformErrorMeta errorMeta = new TransformErrorMeta(source, errorTarget);
errorMeta.setEnabled(true);
source.setTransformErrorMeta(errorMeta);

assertTrue(source.isDoingErrorHandling());
}
}
Loading
Loading