Fix join from orthogonal regions

- Fix a bug where multiple end states in a region failed to complete
  if transition was from a super state which in case of join used to track
  a list of end states. This is now changed to track list of lists and
  we can now track that at least one end state were reached per region.
- Fixes #619
This commit is contained in:
Janne Valkealahti
2019-01-13 09:01:20 +00:00
parent 1a485e4d69
commit b30f0ed0b3
6 changed files with 296 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2017 the original author or authors.
* Copyright 2015-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ package org.springframework.statemachine.config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -799,27 +800,29 @@ public abstract class AbstractStateMachineFactory<S, E> extends LifecycleObjectS
} else if (stateData.getPseudoStateKind() == PseudoStateKind.JOIN) {
S s = stateData.getState();
List<S> list = stateMachineTransitions.getJoins().get(s);
List<State<S, E>> joins = new ArrayList<State<S,E>>();
List<List<State<S, E>>> joins = new ArrayList<List<State<S, E>>>();
// if join source is a regionstate, get
// it's end states from regions
// if join source is an orthogonal state, assume we're joining by region end states
// and actually use list of states from each region to support case where one region
// defines multiple end states.
if (list.size() == 1) {
State<S, E> ss1 = stateMap.get(list.get(0));
if (ss1 instanceof RegionState) {
Collection<Region<S, E>> regions = ((RegionState<S, E>)ss1).getRegions();
for (Region<S, E> r : regions) {
List<State<S, E>> j = new ArrayList<State<S, E>>();
Collection<State<S, E>> ss2 = r.getStates();
for (State<S, E> ss3 : ss2) {
if (ss3.getPseudoState() != null && ss3.getPseudoState().getKind() == PseudoStateKind.END) {
joins.add(ss3);
continue;
j.add(ss3);
}
}
joins.add(j);
}
}
} else {
for (S fs : list) {
joins.add(stateMap.get(fs));
joins.add(Collections.singletonList(stateMap.get(fs)));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2018 the original author or authors.
* Copyright 2015-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ package org.springframework.statemachine.state;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.logging.Log;
@@ -37,7 +38,7 @@ import org.springframework.util.Assert;
public class JoinPseudoState<S, E> extends AbstractPseudoState<S, E> {
private final static Log log = LogFactory.getLog(JoinPseudoState.class);
private final List<State<S, E>> joins;
private final List<List<State<S, E>>> joins;
private final JoinTracker tracker;
private final List<JoinStateData<S, E>> joinTargets;
@@ -47,7 +48,7 @@ public class JoinPseudoState<S, E> extends AbstractPseudoState<S, E> {
* @param joins the joins
* @param joinTargets the target states
*/
public JoinPseudoState(List<State<S, E>> joins, List<JoinStateData<S, E>> joinTargets) {
public JoinPseudoState(List<List<State<S, E>>> joins, List<JoinStateData<S, E>> joinTargets) {
super(PseudoStateKind.JOIN);
this.joins = joins;
this.joinTargets = joinTargets;
@@ -79,7 +80,7 @@ public class JoinPseudoState<S, E> extends AbstractPseudoState<S, E> {
*
* @return the joins
*/
public List<State<S, E>> getJoins() {
public List<List<State<S, E>>> getJoins() {
return joins;
}
@@ -104,48 +105,67 @@ public class JoinPseudoState<S, E> extends AbstractPseudoState<S, E> {
private class JoinTracker {
private final List<State<S, E>> track;
private final List<List<State<S, E>>> track;
private volatile boolean notified = false;
public JoinTracker() {
this.track = new ArrayList<State<S,E>>(joins);
for (State<S, E> tt : joins) {
final State<S, E> t = tt;
t.addStateListener(new StateListenerAdapter<S, E>() {
this.track = new ArrayList<List<State<S,E>>>(joins.size());
for (List<State<S, E>> list : joins) {
this.track.add(new ArrayList<State<S,E>>(list));
for (State<S, E> tt : list) {
final State<S, E> t = tt;
t.addStateListener(new StateListenerAdapter<S, E>() {
@Override
public void onComplete(StateContext<S, E> context) {
boolean trackSizeZero = false;
synchronized (track) {
track.remove(t);
if (track.size() == 0) {
trackSizeZero = true;
@Override
public void onComplete(StateContext<S, E> context) {
synchronized (track) {
Iterator<List<State<S, E>>> iterator = track.iterator();
while(iterator.hasNext()) {
List<State<S,E>> next = iterator.next();
if (next.contains(t)) {
iterator.remove();
}
}
}
if (!notified && track.isEmpty()) {
log.debug("Join complete");
notified = true;
notifyContext(new DefaultPseudoStateContext<S, E>(JoinPseudoState.this, PseudoAction.JOIN_COMPLETED));
}
}
if (!notified && trackSizeZero) {
log.debug("Join complete");
notified = true;
notifyContext(new DefaultPseudoStateContext<S, E>(JoinPseudoState.this, PseudoAction.JOIN_COMPLETED));
}
}
});
});
}
}
}
void reset() {
track.clear();
track.addAll(joins);
for (List<State<S, E>> list : joins) {
track.add(new ArrayList<State<S,E>>(list));
}
notified = false;
}
void reset(Collection<S> ids) {
track.clear();
for (State<S, E> j : joins) {
if (!ids.contains(j.getId())) {
track.add(j);
// put pack all as normal reset
reset();
// remove given states to reflect correct join stage
Iterator<List<State<S, E>>> trackIter = track.iterator();
while(trackIter.hasNext()) {
List<State<S, E>> list = trackIter.next();
Iterator<State<S, E>> iterator = list.iterator();
while(iterator.hasNext()) {
State<S, E> next = iterator.next();
if (ids.contains(next.getId())) {
iterator.remove();
}
}
// also remove list if it became empty
if (list.isEmpty()) {
trackIter.remove();
}
}
notified = false;
}
public boolean isNotified() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2018 the original author or authors.
* Copyright 2015-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -242,8 +242,10 @@ public class DefaultStateMachineExecutor<S, E> extends LifecycleObjectSupport im
// special handling of join
if (StateMachineUtils.isPseudoState(t.getTarget(), PseudoStateKind.JOIN)) {
if (joinSyncStates.isEmpty()) {
List<State<S, E>> joins = ((JoinPseudoState<S, E>)t.getTarget().getPseudoState()).getJoins();
joinSyncStates.addAll(joins);
List<List<State<S,E>>> joins = ((JoinPseudoState<S, E>)t.getTarget().getPseudoState()).getJoins();
for (List<State<S,E>> j : joins) {
joinSyncStates.addAll(j);
}
}
joinSyncTransitions.add(t);
boolean removed = joinSyncStates.remove(t.getSource());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2018 the original author or authors.
* Copyright 2015-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -68,13 +68,13 @@ public abstract class AbstractStateMachineTests {
public enum TestStates {
SI,S1,S2,S3,S4,SF,SH,
S10,S11,S101,S111,S112,S12,S121,S122,S13,
S20,S21,S201,S211,S212,
S20,S21,S22,S201,S211,S212,
S1011,S1012,S2011,S2012,
S30,S31,S32,S33
}
public enum TestEvents {
E1,E2,E3,E4,EF,EH
E1,E2,E3,E4,E5,EF,EH
}
public static enum TestStates2 {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018 the original author or authors.
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -173,6 +173,40 @@ public class CompletionEventTests extends AbstractStateMachineTests {
assertThat(machine.getState().getId(), is("S3"));
}
@SuppressWarnings({ "unchecked" })
@Test
public void testRegionWithoutStateActionCompletesWithMultipleEnds1() throws Exception {
context.register(Config6.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<String,String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.start();
assertThat(machine.getState().getId(), is("S1"));
machine.sendEvent(MessageBuilder.withPayload("E1").build());
machine.sendEvent(MessageBuilder.withPayload("E2").build());
assertThat(machine.getState().getId(), is("S3"));
}
@SuppressWarnings({ "unchecked" })
@Test
public void testRegionWithoutStateActionCompletesWithMultipleEnds2() throws Exception {
context.register(Config6.class);
context.refresh();
assertTrue(context.containsBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE));
StateMachine<String,String> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, StateMachine.class);
machine.start();
assertThat(machine.getState().getId(), is("S1"));
machine.sendEvent(MessageBuilder.withPayload("E1").build());
machine.sendEvent(MessageBuilder.withPayload("E3").build());
assertThat(machine.getState().getId(), is("S3"));
}
@Configuration
@EnableStateMachine
static class Config1 extends StateMachineConfigurerAdapter<String, String> {
@@ -397,6 +431,69 @@ public class CompletionEventTests extends AbstractStateMachineTests {
}
}
@Configuration
@EnableStateMachine
static class Config6 extends StateMachineConfigurerAdapter<String, String> {
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states
.withStates()
.initial("S1")
.state("S2")
.state("S3")
.and()
.withStates()
.parent("S2")
.initial("S201")
.state("S202")
.end("S203")
.end("S204")
.and()
.withStates()
.parent("S2")
.initial("S211")
.state("S212")
.end("S213");
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions
.withExternal()
.source("S1")
.target("S2")
.event("E1")
.and()
.withExternal()
.source("S201")
.target("S202")
.and()
.withExternal()
.source("S202")
.target("S203")
.event("E2")
.and()
.withExternal()
.source("S202")
.target("S204")
.event("E3")
.and()
.withExternal()
.source("S211")
.target("S212")
.and()
.withExternal()
.source("S212")
.target("S213")
.and()
.withExternal()
.source("S2")
.target("S3");
}
}
private static class TestCountAction implements Action<String, String> {
int count = 0;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2018 the original author or authors.
* Copyright 2015-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -313,6 +313,72 @@ public class JoinStateTests extends AbstractStateMachineTests {
assertThat("Interceptor postStateChange has null transition", nullCheck.get(), is(false));
}
@Test
@SuppressWarnings("unchecked")
public void testJoinSuperMultipleEnds1() throws Exception {
context.register(BaseConfig.class, Config4.class);
context.refresh();
ObjectStateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
TestListener listener = new TestListener();
machine.addStateListener(listener);
listener.reset(1);
assertThat(machine, notNullValue());
machine.start();
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
listener.reset(3);
machine.sendEvent(TestEvents.E1);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(3));
listener.reset(1);
machine.sendEvent(TestEvents.E2);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
listener.reset(2);
machine.sendEvent(TestEvents.E3);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(2));
assertThat(machine.getState().getIds(), contains(TestStates.S4));
}
@Test
@SuppressWarnings("unchecked")
public void testJoinSuperMultipleEnds2() throws Exception {
context.register(BaseConfig.class, Config4.class);
context.refresh();
ObjectStateMachine<TestStates,TestEvents> machine =
context.getBean(StateMachineSystemConstants.DEFAULT_ID_STATEMACHINE, ObjectStateMachine.class);
TestListener listener = new TestListener();
machine.addStateListener(listener);
listener.reset(1);
assertThat(machine, notNullValue());
machine.start();
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
listener.reset(3);
machine.sendEvent(TestEvents.E1);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(3));
listener.reset(1);
machine.sendEvent(TestEvents.E5);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(1));
listener.reset(2);
machine.sendEvent(TestEvents.E3);
assertThat(listener.stateChangedLatch.await(2, TimeUnit.SECONDS), is(true));
assertThat(listener.stateChangedCount, is(2));
assertThat(machine.getState().getIds(), contains(TestStates.S4));
}
@Configuration
@EnableStateMachine
static class Config1 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@@ -497,6 +563,70 @@ public class JoinStateTests extends AbstractStateMachineTests {
}
@Configuration
@EnableStateMachine
static class Config4 extends EnumStateMachineConfigurerAdapter<TestStates, TestEvents> {
@Override
public void configure(StateMachineStateConfigurer<TestStates, TestEvents> states) throws Exception {
states
.withStates()
.initial(TestStates.SI)
.state(TestStates.S2)
.join(TestStates.S3)
.state(TestStates.S4)
.and()
.withStates()
.parent(TestStates.S2)
.initial(TestStates.S20)
.end(TestStates.S21)
.end(TestStates.S22)
.and()
.withStates()
.parent(TestStates.S2)
.initial(TestStates.S30)
.end(TestStates.S31);
}
@Override
public void configure(StateMachineTransitionConfigurer<TestStates, TestEvents> transitions) throws Exception {
transitions
.withExternal()
.source(TestStates.SI)
.target(TestStates.S2)
.event(TestEvents.E1)
.and()
.withExternal()
.source(TestStates.S20)
.target(TestStates.S21)
.event(TestEvents.E2)
.and()
.withExternal()
.source(TestStates.S20)
.target(TestStates.S22)
.event(TestEvents.E5)
.and()
.withExternal()
.source(TestStates.S30)
.target(TestStates.S31)
.event(TestEvents.E3)
.and()
.withJoin()
.source(TestStates.S2)
.target(TestStates.S3)
.and()
.withExternal()
.source(TestStates.S3)
.target(TestStates.S4)
.and()
.withExternal()
.source(TestStates.S4)
.target(TestStates.SI)
.event(TestEvents.E4);
}
}
private static class TestListener extends StateMachineListenerAdapter<TestStates, TestEvents> {
volatile CountDownLatch stateChangedLatch = new CountDownLatch(1);