Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

Commit

Permalink
Merge pull request #1137 from finos/1087-add-retry-limit-to-reductive…
Browse files Browse the repository at this point in the history
…-walker

1087, 1092 add retry limit to reductive walker
  • Loading branch information
ms14981 authored Jul 23, 2019
2 parents ee15f56 + ff8f23b commit 61d8c5c
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.scottlogic.deg.generator.inputs.validation.ProfileValidator;
import com.scottlogic.deg.generator.utils.JavaUtilRandomNumberGenerator;
import com.scottlogic.deg.generator.walker.DecisionTreeWalker;
import com.scottlogic.deg.generator.walker.ReductiveWalkerRetryChecker;
import com.scottlogic.deg.generator.walker.reductive.IterationVisualiser;

import java.time.OffsetDateTime;
Expand Down Expand Up @@ -70,6 +71,7 @@ protected void configure() {
bind(DataGenerator.class).to(DecisionTreeDataGenerator.class);
bind(DecisionTreeFactory.class).to(MaxStringLengthInjectingDecisionTreeFactory.class);
bind(FieldValueSourceEvaluator.class).to(StandardFieldValueSourceEvaluator.class);
bind(ReductiveWalkerRetryChecker.class).toInstance(new ReductiveWalkerRetryChecker(10000));

bind(JavaUtilRandomNumberGenerator.class)
.toInstance(new JavaUtilRandomNumberGenerator(OffsetDateTime.now().getNano()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@
import com.google.inject.Inject;
import com.scottlogic.deg.generator.decisiontree.DecisionTree;
import com.scottlogic.deg.generator.fieldspecs.RowSpec;
import com.scottlogic.deg.generator.generation.DataGeneratorMonitor;
import com.scottlogic.deg.generator.generation.databags.DataBag;

import java.util.Optional;
import java.util.stream.Stream;

public class RandomReductiveDecisionTreeWalker implements DecisionTreeWalker {
private final ReductiveDecisionTreeWalker underlyingWalker;
private final DataGeneratorMonitor monitor;

@Inject
RandomReductiveDecisionTreeWalker(ReductiveDecisionTreeWalker underlyingWalker) {
RandomReductiveDecisionTreeWalker(ReductiveDecisionTreeWalker underlyingWalker, DataGeneratorMonitor monitor) {
this.underlyingWalker = underlyingWalker;
this.monitor = monitor;
}

@Override
Expand All @@ -49,7 +52,16 @@ public Stream<DataBag> walk(DecisionTree tree) {
}

private Optional<DataBag> getFirstRowSpecFromRandomisingIteration(DecisionTree tree) {
return underlyingWalker.walk(tree)
.findFirst();
try {
return underlyingWalker.walk(tree)
.findFirst();
} catch (RetryLimitReachedException ex) {
monitor.addLineToPrintAtEndOfGeneration("");
monitor.addLineToPrintAtEndOfGeneration("The retry limit for generating data has been hit.");
monitor.addLineToPrintAtEndOfGeneration("This may mean that a lot or all of the profile is contradictory.");
monitor.addLineToPrintAtEndOfGeneration("Either fix the profile, or try running the same command again.");
return Optional.empty();
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class ReductiveDecisionTreeWalker implements DecisionTreeWalker {
private final ReductiveDataGeneratorMonitor monitor;
private final FieldSpecValueGenerator fieldSpecValueGenerator;
private final FixFieldStrategyFactory fixFieldStrategyFactory;
private ReductiveWalkerRetryChecker retryChecker;

@Inject
public ReductiveDecisionTreeWalker(
Expand All @@ -49,13 +50,15 @@ public ReductiveDecisionTreeWalker(
ReductiveDataGeneratorMonitor monitor,
ReductiveTreePruner treePruner,
FieldSpecValueGenerator fieldSpecValueGenerator,
FixFieldStrategyFactory fixFieldStrategyFactory) {
FixFieldStrategyFactory fixFieldStrategyFactory,
ReductiveWalkerRetryChecker retryChecker) {
this.iterationVisualiser = iterationVisualiser;
this.reductiveFieldSpecBuilder = reductiveFieldSpecBuilder;
this.monitor = monitor;
this.treePruner = treePruner;
this.fieldSpecValueGenerator = fieldSpecValueGenerator;
this.fixFieldStrategyFactory = fixFieldStrategyFactory;
this.retryChecker = retryChecker;
}

/* initialise the walker with a set (ReductiveState) of unfixed fields */
Expand All @@ -64,6 +67,7 @@ public Stream<DataBag> walk(DecisionTree tree) {
ReductiveState initialState = new ReductiveState(tree.fields);
visualise(tree.getRootNode(), initialState);
FixFieldStrategy fixFieldStrategy = fixFieldStrategyFactory.create(tree.getRootNode());
retryChecker.reset();
return fixNextField(tree.getRootNode(), initialState, fixFieldStrategy);
}

Expand Down Expand Up @@ -95,6 +99,7 @@ private Stream<DataBag> pruneTreeForNextValue(
if (reducedTree.isContradictory()){
//yielding an empty stream will cause back-tracking
this.monitor.unableToStepFurther(reductiveState);
retryChecker.retryUnsuccessful();
return Stream.empty();
}

Expand All @@ -105,6 +110,7 @@ private Stream<DataBag> pruneTreeForNextValue(
visualise(reducedTree.get(), newReductiveState);

if (newReductiveState.allFieldsAreFixed()) {
retryChecker.retrySuccessful();
return Stream.of(newReductiveState.asDataBag());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.scottlogic.deg.generator.walker;

public class ReductiveWalkerRetryChecker {
private int numRetriesSoFar = 0;
private int retryLimit;

public ReductiveWalkerRetryChecker(int retryLimit) {
this.retryLimit = retryLimit;
}

void retrySuccessful() {
numRetriesSoFar = 0;
}

void retryUnsuccessful() {
numRetriesSoFar++;
if (numRetriesSoFar > retryLimit) {
throw new RetryLimitReachedException();
}
}

void reset() {
numRetriesSoFar = 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.scottlogic.deg.generator.walker;

public class RetryLimitReachedException extends RuntimeException {
// This exception must be a RuntimeException to be able to break out of a stream evaluation.
public RetryLimitReachedException() {
super();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,28 @@
import com.scottlogic.deg.common.profile.ProfileFields;
import com.scottlogic.deg.generator.decisiontree.DecisionTree;
import com.scottlogic.deg.generator.decisiontree.TreeConstraintNode;
import com.scottlogic.deg.generator.generation.DataGeneratorMonitor;
import com.scottlogic.deg.generator.generation.databags.DataBag;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.core.Is.is;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.*;

class RandomReductiveDecisionTreeWalkerTests {
private DecisionTree tree;
private RandomReductiveDecisionTreeWalker walker;
private ReductiveDecisionTreeWalker underlyingWalker;
private DataGeneratorMonitor monitor = mock(DataGeneratorMonitor.class);

@BeforeEach
public void beforeEach(){
Expand All @@ -47,7 +51,7 @@ public void beforeEach(){
);

underlyingWalker = mock(ReductiveDecisionTreeWalker.class);
walker = new RandomReductiveDecisionTreeWalker(underlyingWalker);
walker = new RandomReductiveDecisionTreeWalker(underlyingWalker, monitor);
}

/**
Expand Down Expand Up @@ -99,6 +103,32 @@ public void shouldAccommodateNoDataInSubsequentIteration() {
is(false));
}

@Test
public void getFirstRowSpecFromRandomisingIteration_onRetryFail_returnsEmptyStream() {
when(underlyingWalker.walk(tree)).thenReturn(
Stream.iterate(new DataBag(new HashMap<>()), dataBag -> {
throw new RetryLimitReachedException();
}).skip(1));

DataBag result = walker.walk(tree).findFirst().orElse(null);

verify(underlyingWalker, times(1)).walk(tree);
assertNull(result);

}

@Test
public void getFirstRowSpecFromRandomisingIteration_onRetryFail_reportsError() {
when(underlyingWalker.walk(tree)).thenReturn(
Stream.iterate(new DataBag(new HashMap<>()), dataBag -> {
throw new RetryLimitReachedException();
}).skip(1));

walker.walk(tree).findFirst();

verify(monitor, atLeastOnce()).addLineToPrintAtEndOfGeneration(anyString());
}

private static DataBag rowSpec(String detail) {
return mock(DataBag.class, detail);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import com.scottlogic.deg.common.profile.Field;
import com.scottlogic.deg.common.profile.ProfileFields;
import com.scottlogic.deg.generator.builders.ConstraintNodeBuilder;
import com.scottlogic.deg.generator.decisiontree.ConstraintNode;
import com.scottlogic.deg.generator.decisiontree.DecisionTree;
import com.scottlogic.deg.generator.decisiontree.TreeConstraintNode;
import com.scottlogic.deg.generator.fieldspecs.FieldSpec;
Expand All @@ -26,18 +28,23 @@
import com.scottlogic.deg.generator.generation.NoopDataGeneratorMonitor;
import com.scottlogic.deg.generator.generation.databags.DataBag;
import com.scottlogic.deg.generator.generation.databags.DataBagValue;
import com.scottlogic.deg.generator.walker.reductive.*;
import com.scottlogic.deg.generator.walker.reductive.Merged;
import com.scottlogic.deg.generator.walker.reductive.NoOpIterationVisualiser;
import com.scottlogic.deg.generator.walker.reductive.ReductiveFieldSpecBuilder;
import com.scottlogic.deg.generator.walker.reductive.ReductiveTreePruner;
import com.scottlogic.deg.generator.walker.reductive.fieldselectionstrategy.FixFieldStrategy;
import com.scottlogic.deg.generator.walker.reductive.fieldselectionstrategy.FixFieldStrategyFactory;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.hamcrest.collection.IsEmptyCollection.empty;
import static java.time.Duration.ofMillis;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

Expand All @@ -49,6 +56,8 @@ class ReductiveDecisionTreeWalkerTests {
private FixFieldStrategy fixFieldStrategy;
private FixFieldStrategyFactory fixFieldStrategyFactory;
private FieldSpecValueGenerator fieldSpecValueGenerator;
private ReductiveTreePruner treePruner;
private ReductiveWalkerRetryChecker retryChecker = new ReductiveWalkerRetryChecker(100);
private Field field1 = new Field("field1");
private Field field2 = new Field("field2");

Expand All @@ -57,23 +66,24 @@ public void beforeEach(){
ProfileFields fields = new ProfileFields(Arrays.asList(field1, field2));
rootNode = new TreeConstraintNode();
tree = new DecisionTree(rootNode, fields);
ReductiveTreePruner treePruner = mock(ReductiveTreePruner.class);
when(treePruner.pruneConstraintNode(eq(rootNode), any(), any())).thenReturn(Merged.of(rootNode));

reductiveFieldSpecBuilder = mock(ReductiveFieldSpecBuilder.class);
fieldSpecValueGenerator = mock(FieldSpecValueGenerator.class);
fixFieldStrategy = mock(FixFieldStrategy.class);
when(fixFieldStrategy.getNextFieldToFix(any())).thenReturn(field1, field2);
fixFieldStrategyFactory = mock(FixFieldStrategyFactory.class);
when(fixFieldStrategyFactory.create(any())).thenReturn(fixFieldStrategy);
treePruner = mock(ReductiveTreePruner.class);
when(treePruner.pruneConstraintNode(eq(rootNode), any(), any())).thenReturn(Merged.of(rootNode));

walker = new ReductiveDecisionTreeWalker(
new NoOpIterationVisualiser(),
reductiveFieldSpecBuilder,
new NoopDataGeneratorMonitor(),
treePruner,
fieldSpecValueGenerator,
fixFieldStrategyFactory
fixFieldStrategyFactory,
retryChecker
);
}

Expand All @@ -87,7 +97,7 @@ public void shouldReturnEmptyCollectionOfRowsWhenFirstFieldCannotBeFixed() {
List<DataBag> result = walker.walk(tree).collect(Collectors.toList());

verify(reductiveFieldSpecBuilder).getDecisionFieldSpecs(eq(rootNode), any());
Assert.assertThat(result, empty());
assertThat(result, empty());
}

/**
Expand All @@ -106,6 +116,34 @@ public void shouldReturnEmptyCollectionOfRowsWhenSecondFieldCannotBeFixed() {
List<DataBag> result = walker.walk(tree).collect(Collectors.toList());

verify(reductiveFieldSpecBuilder, times(2)).getDecisionFieldSpecs(eq(rootNode), any());
Assert.assertThat(result, empty());
assertThat(result, empty());
}

@Test
public void walk_whereFirstFieldCannotBeFixed_throwsException() {
ProfileFields fields = new ProfileFields(Arrays.asList(field1, field2));
FieldSpec firstFieldSpec = FieldSpec.Empty.withNotNull();
FieldSpec secondFieldSpec = FieldSpec.Empty.withNotNull();
Set<FieldSpec> fieldSpecs = new HashSet<>();
fieldSpecs.add(firstFieldSpec);
fieldSpecs.add(secondFieldSpec);
ConstraintNode root = ConstraintNodeBuilder.constraintNode()
.where(field1).isNull()
.where(field1).isNotNull()
.build();
DecisionTree tree = new DecisionTree(root, fields);
DataBagValue dataBagValue = mock(DataBagValue.class);
when(fixFieldStrategy.getNextFieldToFix(any())).thenReturn(field1, field2);
when(fixFieldStrategyFactory.create(any())).thenReturn(fixFieldStrategy);
when(treePruner.pruneConstraintNode(eq(root), any(), any())).thenReturn(Merged.of(root));
when(reductiveFieldSpecBuilder.getDecisionFieldSpecs(any(), any())).thenReturn(fieldSpecs);
when(treePruner.pruneConstraintNode(eq(root), any(), any())).thenReturn(Merged.contradictory());

Stream<DataBagValue> infiniteStream = Stream.iterate(dataBagValue, i -> dataBagValue);
when(fieldSpecValueGenerator.generate(anySetOf(FieldSpec.class))).thenReturn(infiniteStream);

assertTimeoutPreemptively(ofMillis(100), () -> {
assertThrows(RetryLimitReachedException.class, () -> walker.walk(tree).findFirst());
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.scottlogic.deg.generator.walker;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class ReductiveWalkerRetryCheckerTests {
@Test
public void retryChecker_withRepeatedFailure_throwsException() {
//Arrange
ReductiveWalkerRetryChecker retryChecker = new ReductiveWalkerRetryChecker(2);

//Act
retryChecker.retryUnsuccessful();
retryChecker.retryUnsuccessful();

//Assert
assertThrows(RetryLimitReachedException.class, retryChecker::retryUnsuccessful);
}

@Test
public void retryChecker_withRepeatedFailureAfterSuccess_doesNotThrowException() {
//Arrange
ReductiveWalkerRetryChecker retryChecker = new ReductiveWalkerRetryChecker(2);

//Act
retryChecker.retryUnsuccessful();
retryChecker.retrySuccessful();
retryChecker.retryUnsuccessful();

//Assert
assertDoesNotThrow(retryChecker::retryUnsuccessful);
}

@Test
public void reset_resetsLimit() {
//Arrange
ReductiveWalkerRetryChecker retryChecker = new ReductiveWalkerRetryChecker(2);

//Act
retryChecker.retryUnsuccessful();
retryChecker.retryUnsuccessful();
retryChecker.reset();
retryChecker.retryUnsuccessful();
retryChecker.retryUnsuccessful();

//Assert
assertThrows(RetryLimitReachedException.class, retryChecker::retryUnsuccessful);
}

@Test
public void reset_resetsGuaranteeOfSuccess() {
//Arrange
ReductiveWalkerRetryChecker retryChecker = new ReductiveWalkerRetryChecker(2);

//Act
retryChecker.retrySuccessful();
retryChecker.reset();
retryChecker.retryUnsuccessful();
retryChecker.retryUnsuccessful();

//Assert
assertThrows(RetryLimitReachedException.class, retryChecker::retryUnsuccessful);
}
}
Loading

0 comments on commit 61d8c5c

Please sign in to comment.