1

Is there a way to achieve the following logic with JSR 352 Batch API? I have a series of Steps that each need to be executed based on a different condition known when starting the job. ConditionsEntity is provided by an external system.

public List<Steps> createStepSequence(ConditionsEntity conditions) {
  if (conditions.isStep1Enabled()) {
    steps.add(step1)
  }
  if (conditions.isStep2Enabled()) {
    steps.add(step2)
  }
  if (conditions.isStep3Enabled()) {
    steps.add(step3)
  }
  //many more ifs

return steps;
}

My first attempt fails because of: com.ibm.jbatch.container.exception.BatchContainerRuntimeException: A decision cannot precede another decision. I'm adding the FAILING Code here

<?xml version="1.0" encoding="UTF-8"?>
<job id="myJob" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/jobXML_1_0.xsd" version="1.0">
    <properties>
        <property name="isExecuteStep2" value="false"/>
        <property name="isExecuteStep3" value="false"/>
    </properties>
    <step id="step1" next="decider1">
        <batchlet ref="myBatchlet1"/>
    </step>
    <decision id="decider1" ref="SkipNextStepDecider">
        <properties>
            <property name="isExecuteNextStep" value="#{jobProperties['isExecuteStep2']}"/>
        </properties>
        <next on="EXECUTE" to="step2"/>
        <next on="SKIP" to="decider2"/>
    </decision>
    <step id="step2">
        <batchlet ref="myBatchlet2"/>
    </step>
    <decision id="decider2" ref="SkipNextStepDecider">
        <properties>
            <property name="isExecuteNextStep" value="#{jobProperties['isExecuteStep3']}"/>
        </properties>
        <next on="EXECUTE" to="step3"/>
        <end on="SKIP"/>
    </decision>
    <step id="step3">
        <batchlet ref="myBatchlet3"/>
    </step>
</job>


@Named
public class SkipNextStepDecider implements Decider {

    @Inject
    @BatchProperty
    private String isExecuteNextStep;

    @Override
    public String decide(StepExecution[] ses) throws Exception {
        if (isExecuteNextStep.equalsIgnoreCase("true")) {
            return "EXECUTE";
        } else {
            return "SKIP";
        }
    }
}

UPDATE I have implemented the following suggested solution with a passThroughStep. It's working correctly, but I would still love to be able to avoid all this code duplication.

<?xml version="1.0" encoding="UTF-8"?>
<job id="decisionpoc" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
    <step id="dummy0" next="decider1">
        <batchlet ref="dummyBatchlet"/>
    </step>
    <decision id="decider1" ref="skipNextStepDecider">
        <properties>
            <property name="condition" value="isExecuteStep1"/>
        </properties>
        <next on="EXECUTE" to="step1"/>
        <next on="SKIP" to="dummy1"/>
    </decision>
    <step id="step1" next="decider2">
        <batchlet ref="myBatchlet1"/>
    </step>
    <step id="dummy1" next="decider2">
        <batchlet ref="dummyBatchlet"/>
    </step>
    <decision id="decider2" ref="skipNextStepDecider">
        <properties>
            <property name="condition" value="isExecuteStep2"/>
        </properties>
        <next on="EXECUTE" to="step2"/>
        <next on="SKIP" to="dummy2"/>
    </decision>
    <step id="step2">
        <batchlet ref="myBatchlet2"/>
    </step>
    <step id="dummy2" next="decider3">
        <batchlet ref="dummyBatchlet"/>
    </step>
    <decision id="decider3" ref="skipNextStepDecider">
        <properties>
            <property name="condition" value="isExecuteStep3"/>
        </properties>
        <next on="EXECUTE" to="step3"/>
        <end on="SKIP"/>
    </decision>
    <step id="step3">
        <batchlet ref="myBatchlet3"/>
    </step>
</job>

The Decider

@Named
public class SkipNextStepDecider implements Decider {

    @Inject
    @BatchProperty
    private String condition;

    @Inject
    private JobContext jobContext;

    @Override
    public String decide(StepExecution[] ses) throws Exception {
        Properties parameters = getParameters();
        String isExecuteNextStep = parameters.getProperty(condition);
        if (isExecuteNextStep.equalsIgnoreCase("true")) {
            return "EXECUTE";
        } else {
            return "SKIP";
        }
    }

    private Properties getParameters() {
        JobOperator operator = getJobOperator();
        return operator.getParameters(jobContext.getExecutionId());

    }
}

My Test

public class DecisionPOCTest extends AbstractBatchLOT {

    @Test
    public void testProcess() throws Exception {
        JobOperator jobOperator = getJobOperator();
        Properties properties = new Properties();
        properties.setProperty("isExecuteStep1", "true");
        properties.setProperty("isExecuteStep2", "false");
        properties.setProperty("isExecuteStep3", "true");
        Long executionId = jobOperator.start("poc/decisionPOC", properties);
        JobExecution jobExecution = jobOperator.getJobExecution(executionId);

        jobExecution = BatchTestHelper.keepTestAlive(jobExecution);


        List<StepExecution> stepExecutions = jobOperator.getStepExecutions(executionId);
        List<String> executedSteps = new ArrayList<>();
        for (StepExecution stepExecution : stepExecutions) {
            executedSteps.add(stepExecution.getStepName());
        }

        assertEquals(COMPLETED, jobExecution.getBatchStatus());
        assertEquals(4, stepExecutions.size());
        assertArrayEquals(new String[]{"dummy0", "step1", "dummy2", "step3"}, executedSteps.toArray());
        assertFalse(executedSteps.contains("step2"));
    }
}
taranaki
  • 778
  • 3
  • 9
  • 28
  • I am using Websphere Liberty. I just noticed that this might be relevant as the specification says that it should be possible to go from a decision to another decision. – taranaki Dec 06 '19 at 13:17
  • Yes, that is relevant. Please open an issue against Open Liberty (which I work on): https://github.com/openliberty/open-liberty/issues. – Scott Kurz Dec 06 '19 at 15:10
  • I didn't realize we had a specification statement to support the notion that this should be possible (@cheng pointed this out by quoting this... but it went right over my head since I was looking at the Decider javadoc (https://jakarta.ee/specifications/platform/8/apidocs/javax/batch/api/Decider.html#decide-javax.batch.runtime.StepExecution:A-) and noting that the "transition from decision" case wasn't described there. If it's in the spec though, I think we can support the case for adding function to Open Liberty. We should think carefully about whether a StepExecution gets passed or not. – Scott Kurz Dec 06 '19 at 15:17
  • @ScottKurz I have added the issue: https://github.com/OpenLiberty/open-liberty/issues/10102 – taranaki Dec 10 '19 at 08:15

4 Answers4

2

It looks like the failure was caused by the fact that one decision had another decison as its next execution point at runtime. As per the JSR 352 spec Section 8.5, it should be a supported use case:

A job may contain any number of decision elements. A decision element is the target of the "next" attribute from a job-level step, flow, split, or another decision.

As a workaround, you can try having a pass-through batchlet-step that contains the same condition and logic. For example,

<step id="pass-through-step">
   <batchlet ref="PassThroughBatchlet"/>
   <next on="EXECUTE" to="step2"/>
   <next on="SKIP" to="decider2"/>
</step>

Or if some of your conditional logic can be achived with a batchlet-step containing transition elements, you can do away with those decisions.

cheng
  • 1,076
  • 6
  • 6
  • Hi cheng. Yes this works (I've added the full POC for this in my Update). It's a bit of a pity that this seems to be the best option as it's really not resulting in nice xml :/ – taranaki Dec 06 '19 at 13:07
  • btw I'm also a bit confused regarding the specification... In section 8.6.1 it does not show the possibility of adding a decisionId as next element: – taranaki Dec 06 '19 at 13:20
1

@cheng has a good answer which would be a very small change from what you're doing (you just need to change your Decider to a Batchlet basically).

To me, at least, it's an interesting question to consider what other options the spec gives you here. Another would be to have a single decision with a Decider with ALL "isExecuteStepNN" props injected into it, that you could call after each step. That Decider gets passed a StepExecution so you know what the previous step is, and you could combine that with the "isExecute..." props to have the Decider return the id of the next step to execute.

Though that may be clever, I think cheng's answer is an easier workaround. I also think the spec should consider allowing this. Probably the reason for not supporting this is to avoid answering the question: "what StepExecution(s) should be passed to the decide method?" which seems solvable.

Scott Kurz
  • 4,985
  • 1
  • 18
  • 40
  • Hi Scott. I've also had the idea to implement the logic in this way. I think the xml will also start to look super ugly as I'd need the "Decided" before each Step element and the earlier the decides appears, the longer the list of possible next values (basically every step afterwards might be next). – taranaki Dec 06 '19 at 13:06
  • Or is it possible to return the ID of the step that's to be executed next in the Decider? – taranaki Dec 06 '19 at 13:11
  • I've asked this question separately so as to not clutter this thread: https://stackoverflow.com/questions/59214372/jsr352-decide-next-step-based-on-return-parameter-from-decider – taranaki Dec 06 '19 at 13:44
  • Yes, agree the XML would be ugly... but it'd be in one place, rather than repeated. I don't think there's a great answer to the other question, but thanks for breaking it out. – Scott Kurz Dec 06 '19 at 15:18
1

I have another possible solution to the problem that has different drawbacks than the other proposed solutions.

It's possible to let the Step decide for itself whether it needs to execute anything or not.

The xml looks much neater:

<?xml version="1.0" encoding="UTF-8"?>
<job id="decisionpoc" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
    <step id="step1" next="step2">
        <batchlet ref="myBatchletWithDecision1">
            <properties>
                <property name="condition" value="isExecuteStep1"/>
            </properties>
        </batchlet>
    </step>
    <step id="step2" next="step3">
        <batchlet ref="myBatchletWithDecision2">
            <properties>
                <property name="condition" value="isExecuteStep2"/>
            </properties>
        </batchlet>
    </step>
    <step id="step3">
        <batchlet ref="myBatchletWithDecision3">
            <properties>
                <property name="condition" value="isExecuteStep3"/>
            </properties>
        </batchlet>
    </step>
</job>

The Batchlet then look as follows:

@Named
public class MyBatchletWithDecision1 extends AbstractBatchlet {

    @Inject
    @BatchProperty
    private String condition;

    @Inject
    private JobContext jobContext;

    @Override
    public String process() {
        Properties parameters = getParameters();
        String isExecuteStep = parameters.getProperty(condition);
        if (isExecuteStep.equalsIgnoreCase("true")) {
            System.out.println("Running inside a batchlet 1");
        } else {
            //TODO somehow log that the step was skipped
        }
        return "COMPLETED";
    }

    private Properties getParameters() {
        JobOperator operator = getJobOperator();
        return operator.getParameters(jobContext.getExecutionId());

    }
}

This Test somewhat doesn't really test the expected behaviour yet. I actually want to skip Step2, but with the current solution step2 does get executed, but doesn't do anything. I haven't added functionality to test for this yet.

@Test
public void testProcess() throws Exception {
    JobOperator jobOperator = getJobOperator();
    Properties properties = new Properties();
    properties.setProperty("isExecuteStep1", "true");
    properties.setProperty("isExecuteStep2", "false");
    properties.setProperty("isExecuteStep3", "true");
    Long executionId = jobOperator.start("poc/decisionWithoutDeciderPOC", properties);
    JobExecution jobExecution = jobOperator.getJobExecution(executionId);

    jobExecution = BatchTestHelper.keepTestAlive(jobExecution);


    List<StepExecution> stepExecutions = jobOperator.getStepExecutions(executionId);
    List<String> executedSteps = new ArrayList<>();
    for (StepExecution stepExecution : stepExecutions) {
        executedSteps.add(stepExecution.getStepName());
    }

    assertEquals(COMPLETED, jobExecution.getBatchStatus());
    assertEquals(3, stepExecutions.size());
    assertArrayEquals(new String[]{"step1", "step2", "step3"}, executedSteps.toArray());
}
taranaki
  • 778
  • 3
  • 9
  • 28
  • A couple quick thoughts: 1. Moving control flow into the logic of your (non-Decider) step artifacts can have the downside of making it harder for them to be composed into other jobs (by a JSL / job designer). If, this isn't a concern and/or the author of the Java artifacts is typically also authoring the XML then maybe you're OK with that. 2. Be careful with JobOperator.getParameters() this only gets the job parameters passed with job start/submit. It doesn't look at job properties whose values can (more flexibly) come from job parameters, and element-scoped job properties, etc. – Scott Kurz Dec 06 '19 at 16:08
0

I have just found another possible way to solve the issue that generates a much easier to understand xml file. It avoids duplication xml, doesn't rely on dummy steps and avoids having to move if/else logic to the Batchlet. The basic approach is to create one decision and keep passing control back to this decision after each step that gets executed. (Apparently the same decision can get executed multiple times.)

<?xml version="1.0" encoding="UTF-8"?>
<job id="decisionpoc" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
    <!--    This dummy step is needed because it's not possible to start with a decision-->
    <step id="dummy0" next="decider">
        <batchlet ref="dummyBatchlet"/>
    </step>
    <decision id="decider" ref="nextStepDecider">
        <properties>
            <property name="condition" value="isExecuteSteps"/>
        </properties>
<!--        Need to list all steps, see https://stackoverflow.com/questions/59214372/jsr352-decide-next-step-based-on-return-parameter-from-decider-->
        <next on="STEP1" to="step1"/>
        <next on="STEP2" to="step2"/>
        <next on="STEP3" to="step3"/>
        <end on="SKIP"/>
    </decision>
    <step id="step1" next="decider">
        <batchlet ref="myBatchlet1"/>
    </step>
    <step id="step2" next="decider">
        <batchlet ref="myBatchlet2"/>
    </step>
    <step id="step3">
        <batchlet ref="myBatchlet3"/>
    </step>
</job>

The Decider (please note I just quickly hacked the logic for the POC, don't use this code directly):

@Named
public class NextStepDecider implements Decider {

    @Inject
    @BatchProperty
    private String condition;

    @Inject
    private JobContext jobContext;

    @Override
    public String decide(StepExecution[] ses) throws Exception {
        //FIXME: very hacky code in this method
        if (ses.length != 1) {
            // Decider not reached by transitioning from a step
            return "ERROR";
        }

        Properties parameters = getParameters();
        String executeSteps = parameters.getProperty(condition);
        String[] steps = executeSteps.split(",");

        int start = 0;

        //advance start index to the next step based on the previous step that was executed
        String previousStepName = ses[0].getStepName();
        if (previousStepName.startsWith("step")) {
            start = convertCharToInt(previousStepName);
        }

        //Loop through the remaining steps until we find a step that has its executeStep property set to true
        for (int i = start; i < steps.length; i++) {
            if (steps[i].equalsIgnoreCase("true")) {
                return "STEP" + (i + 1);
            }
        }

        return "SKIP";
    }

    private Properties getParameters() {
        JobOperator operator = getJobOperator();
        return operator.getParameters(jobContext.getExecutionId());
    }

    private int convertCharToInt(String previousStepName) {
        return previousStepName.charAt(previousStepName.length()-1) - '0';
    }
}

The Test:

@Test
public void testProcess() throws Exception {
    JobOperator jobOperator = getJobOperator();
    Properties properties = new Properties();
    properties.setProperty("isExecuteSteps", "true,false,true");
    Long executionId = jobOperator.start("poc/decisionWithDeciderPOC", properties);
    JobExecution jobExecution = jobOperator.getJobExecution(executionId);

    jobExecution = BatchTestHelper.keepTestAlive(jobExecution);


    List<StepExecution> stepExecutions = jobOperator.getStepExecutions(executionId);
    List<String> executedSteps = new ArrayList<>();
    for (StepExecution stepExecution : stepExecutions) {
        executedSteps.add(stepExecution.getStepName());
    }

    assertEquals(COMPLETED, jobExecution.getBatchStatus());
    assertEquals(3, stepExecutions.size());
    assertArrayEquals(new String[]{"dummy0", "step1", "step3"}, executedSteps.toArray());
    assertFalse(executedSteps.contains("step2"));
}
taranaki
  • 778
  • 3
  • 9
  • 28