Skip to content

Commit 86a5efa

Browse files
committed
add validator to validate percentage of record change
1 parent a7140bd commit 86a5efa

File tree

2 files changed

+311
-0
lines changed

2 files changed

+311
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package com.netflix.hollow.api.producer.validation;
2+
3+
import com.netflix.hollow.api.consumer.data.AbstractHollowDataAccessor;
4+
import com.netflix.hollow.api.producer.HollowProducer;
5+
import com.netflix.hollow.core.read.engine.HollowReadStateEngine;
6+
import com.netflix.hollow.core.read.engine.HollowTypeReadState;
7+
8+
import java.util.function.Supplier;
9+
import java.util.logging.Logger;
10+
11+
import static java.util.Objects.requireNonNull;
12+
13+
/**
14+
* Validate the percentage of record change is within the {@link Threshold},
15+
* this only works for types that have a primary key definition
16+
*/
17+
public class RecordCountPercentChangeValidator implements ValidatorListener {
18+
private static final String NAME = RecordCountVarianceValidator.class.getName();
19+
private final Logger log = Logger.getLogger(RecordCountPercentChangeValidator.class.getName());
20+
21+
private final String typeName;
22+
private final Threshold threshold;
23+
private AbstractHollowDataAccessor accessor;
24+
25+
public RecordCountPercentChangeValidator(String typeName,
26+
Threshold threshold) {
27+
this.typeName = typeName;
28+
this.threshold = threshold;
29+
}
30+
31+
@Override
32+
public String getName() {
33+
return NAME;
34+
}
35+
36+
@Override
37+
public ValidationResult onValidate(HollowProducer.ReadState readState) {
38+
HollowReadStateEngine readStateEngine = requireNonNull(readState.getStateEngine(), "read state is null");
39+
HollowTypeReadState typeState = requireNonNull(readStateEngine.getTypeState(typeName),
40+
"type not loaded or does not exist in dataset; type=" + typeName);
41+
accessor =
42+
new AbstractHollowDataAccessor<Object>(readStateEngine, typeName) {
43+
@Override public Object getRecord(int ordinal) { return null; }
44+
};
45+
ValidationResult validationResult = validateChanges(typeState);
46+
log.info(validationResult.toString());
47+
return validationResult;
48+
}
49+
50+
private ValidationResult validateChanges(HollowTypeReadState typeState) {
51+
if(typeState.getPreviousOrdinals().isEmpty()) {
52+
return ValidationResult.from(this).passed("Ignore the check if previous records are empty.");
53+
}
54+
int addRecordNumber = accessor.getAddedRecords().size();
55+
int removeRecordNumber = accessor.getRemovedRecords().size();
56+
int updatedRecordNumber = accessor.getUpdatedRecords().size();
57+
int previousRecordNumber = typeState.getPreviousOrdinals().cardinality();
58+
59+
float addedPercent = (float) addRecordNumber / previousRecordNumber;
60+
float removedPercent = (float) removeRecordNumber / previousRecordNumber;
61+
float updatedPercent = (float) updatedRecordNumber / previousRecordNumber;
62+
63+
64+
float addedPercentageThreshold = threshold.addedPercentageThreshold.get();
65+
float removedPercentageThreshold = threshold.removedPercentageThreshold.get();
66+
float updatedPercentageThreshold = threshold.updatedPercentageThreshold.get();
67+
68+
ValidationResult.ValidationResultBuilder builder = ValidationResult.from(this);
69+
builder.detail("addedRecordNumber", addRecordNumber);
70+
builder.detail("removedRecordNumber", removeRecordNumber);
71+
builder.detail("updatedRecordNumber", updatedRecordNumber);
72+
builder.detail("previousRecordNumber", previousRecordNumber);
73+
builder.detail("addedPercentageThreshold", addedPercentageThreshold);
74+
builder.detail("removedPercentageThreshold", removedPercentageThreshold);
75+
builder.detail("updatedPercentageThreshold", updatedPercentageThreshold);
76+
77+
boolean pass =
78+
(addedPercentageThreshold < 0 || addedPercent < addedPercentageThreshold) &&
79+
(removedPercentageThreshold < 0 || removedPercent < removedPercentageThreshold) &&
80+
(updatedPercentageThreshold < 0 || updatedPercent < updatedPercentageThreshold);
81+
if (pass) {
82+
return builder.passed();
83+
}
84+
return builder.failed("record count change is more than threshold");
85+
}
86+
87+
/**
88+
* Define the percentage of value change as supplier of float in this class,
89+
* for example 1% should be defined as 0.01.
90+
* Not all three threshold needs to be defined. removedPercentageThreshold and updatedPercentageThreshold
91+
* value range should be [0,1], addedPercentageThreshold should not be less than 0.
92+
*/
93+
public static class Threshold {
94+
private final Supplier<Float> removedPercentageThreshold;
95+
private final Supplier<Float> addedPercentageThreshold;
96+
private final Supplier<Float> updatedPercentageThreshold;
97+
98+
public Threshold(Supplier<Float> removedPercentageThreshold,
99+
Supplier<Float> addedPercentageThreshold,
100+
Supplier<Float> updatedPercentageThreshold) {
101+
this.removedPercentageThreshold = removedPercentageThreshold;
102+
this.addedPercentageThreshold = addedPercentageThreshold;
103+
this.updatedPercentageThreshold = updatedPercentageThreshold;
104+
}
105+
106+
107+
public static ThresholdBuilder builder() {
108+
return new ThresholdBuilder();
109+
}
110+
111+
public static class ThresholdBuilder {
112+
private Supplier<Float> removedPercentageThreshold = () -> -1f;
113+
private Supplier<Float> addedPercentageThreshold = () -> -1f;
114+
private Supplier<Float> updatedPercentageThreshold = () -> -1f;
115+
116+
public ThresholdBuilder withRemovedPercentageThreshold(Supplier<Float> removedPercentageThreshold) {
117+
this.removedPercentageThreshold = removedPercentageThreshold;
118+
return this;
119+
}
120+
121+
public ThresholdBuilder withAddedPercentageThreshold(Supplier<Float> addedPercentageThreshold) {
122+
this.addedPercentageThreshold = addedPercentageThreshold;
123+
return this;
124+
}
125+
126+
public ThresholdBuilder withUpdatedPercentageThreshold(Supplier<Float> updatedPercentageThreshold) {
127+
this.updatedPercentageThreshold = updatedPercentageThreshold;
128+
return this;
129+
}
130+
131+
public Threshold build() {
132+
if (removedPercentageThreshold != null && (removedPercentageThreshold.get() < 0 || removedPercentageThreshold.get() > 1)) {
133+
throw new RuntimeException("removed percentage threshold must be between 0 and 1, value "
134+
+ removedPercentageThreshold.get() + " is invalid.");
135+
}
136+
if (updatedPercentageThreshold != null && (updatedPercentageThreshold.get() < 0 || updatedPercentageThreshold.get() > 1)) {
137+
throw new RuntimeException("updated percentage threshold must be between 0 and 1, value "
138+
+ updatedPercentageThreshold.get() + " is invalid.");
139+
}
140+
if (addedPercentageThreshold != null && addedPercentageThreshold.get() < 0) {
141+
throw new RuntimeException("added percentage threshold must be >= 0, value "
142+
+ addedPercentageThreshold.get() + " is invalid.");
143+
}
144+
if (removedPercentageThreshold == null) {
145+
removedPercentageThreshold = () -> -1f;
146+
}
147+
if (updatedPercentageThreshold == null) {
148+
updatedPercentageThreshold = () -> -1f;
149+
}
150+
if (addedPercentageThreshold == null) {
151+
addedPercentageThreshold = () -> -1f;
152+
}
153+
return new Threshold(removedPercentageThreshold,
154+
addedPercentageThreshold,
155+
updatedPercentageThreshold);
156+
}
157+
}
158+
}
159+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package com.netflix.hollow.api.producer.validation;
2+
3+
import com.netflix.hollow.api.producer.HollowProducer;
4+
import com.netflix.hollow.api.producer.fs.HollowInMemoryBlobStager;
5+
import com.netflix.hollow.core.write.objectmapper.HollowPrimaryKey;
6+
import com.netflix.hollow.test.InMemoryBlobStore;
7+
import org.junit.Assert;
8+
import org.junit.Before;
9+
import org.junit.Test;
10+
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
14+
public class RecordCountPercentChangeValidatorTests {
15+
private InMemoryBlobStore blobStore;
16+
17+
@Before
18+
public void setUp() {
19+
blobStore = new InMemoryBlobStore();
20+
}
21+
22+
@Test
23+
public void testPassLessThanThreshold() {
24+
try {
25+
testHelper(RecordCountPercentChangeValidator.Threshold.builder()
26+
.withAddedPercentageThreshold(() -> 0.5f)
27+
.build(), 0, 0, 0);
28+
} catch (Exception e) {
29+
Assert.fail(); //should not reach here
30+
}
31+
}
32+
33+
@Test
34+
public void testPassThresholdNotSet() {
35+
try {
36+
testHelper(RecordCountPercentChangeValidator.Threshold.builder()
37+
.withAddedPercentageThreshold(() -> 0.5f)
38+
.build(), 0, 0, 3);
39+
} catch (Exception e) {
40+
Assert.fail(); //should not reach here
41+
}
42+
}
43+
44+
@Test
45+
public void testAddExceedThreshold() {
46+
try {
47+
testHelper(RecordCountPercentChangeValidator.Threshold.builder()
48+
.withAddedPercentageThreshold(() -> 0.5f)
49+
.build(), 0, 4, 0);
50+
Assert.fail();
51+
} catch (ValidationStatusException expected) {
52+
Assert.assertEquals(1, expected.getValidationStatus().getResults().size());
53+
}
54+
}
55+
56+
@Test
57+
public void testRemoveExceedThreshold() {
58+
try {
59+
testHelper(RecordCountPercentChangeValidator.Threshold.builder()
60+
.withRemovedPercentageThreshold(() -> 0.5f)
61+
.build(), 0, 0, 4);
62+
Assert.fail();
63+
} catch (ValidationStatusException expected) {
64+
Assert.assertEquals(1, expected.getValidationStatus().getResults().size());
65+
}
66+
}
67+
68+
@Test
69+
public void testUpdateExceedThreshold() {
70+
try {
71+
testHelper(RecordCountPercentChangeValidator.Threshold.builder()
72+
.withUpdatedPercentageThreshold(() ->0.5f)
73+
.withAddedPercentageThreshold(() -> 0.5f)
74+
.withAddedPercentageThreshold(() -> 0.5f)
75+
.build(), 4, 1, 1);
76+
Assert.fail();
77+
} catch (ValidationStatusException expected) {
78+
Assert.assertEquals(1, expected.getValidationStatus().getResults().size());
79+
}
80+
}
81+
82+
private void testHelper(RecordCountPercentChangeValidator.Threshold threshold,
83+
int updatedRecordCount,
84+
int addedRecordCount,
85+
int removedRecordCount) {
86+
HollowProducer producer = HollowProducer.withPublisher(blobStore)
87+
.withBlobStager(new HollowInMemoryBlobStager())
88+
.withListener(new RecordCountPercentChangeValidator("TypeWithPrimaryKey", threshold)).build();
89+
90+
List<TypeWithPrimaryKey> previousItems = new ArrayList<>();
91+
previousItems.add(new TypeWithPrimaryKey(0, "a", "aa"));
92+
previousItems.add(new TypeWithPrimaryKey(1, "a", "aa"));
93+
previousItems.add(new TypeWithPrimaryKey(2, "a", "aa"));
94+
previousItems.add(new TypeWithPrimaryKey(3, "a", "aa"));
95+
previousItems.add(new TypeWithPrimaryKey(4, "a", "aa"));
96+
97+
producer.runCycle(new HollowProducer.Populator() {
98+
public void populate(HollowProducer.WriteState newState) throws Exception {
99+
for (TypeWithPrimaryKey val : previousItems) {
100+
newState.add(val);
101+
}
102+
}
103+
});
104+
105+
List<TypeWithPrimaryKey> currentItems = new ArrayList<>();
106+
107+
currentItems.addAll(previousItems);
108+
109+
if (addedRecordCount > 0) {
110+
for (int i = 0; i < addedRecordCount; i++) {
111+
currentItems.add(new TypeWithPrimaryKey(i, String.valueOf(i), String.valueOf(i)));
112+
}
113+
}
114+
115+
if (removedRecordCount > 0) {
116+
for (int i = 0; i < removedRecordCount; i++) {
117+
currentItems.remove(0);
118+
}
119+
}
120+
121+
if (updatedRecordCount > 0) {
122+
for (int i = 0; i < updatedRecordCount; i++) {
123+
TypeWithPrimaryKey item = currentItems.get(0);
124+
TypeWithPrimaryKey newItem = new TypeWithPrimaryKey(item.id, item.name, "bb");
125+
currentItems.remove(0);
126+
currentItems.add(newItem);
127+
}
128+
}
129+
130+
producer.runCycle(new HollowProducer.Populator() {
131+
public void populate(HollowProducer.WriteState newState) throws Exception {
132+
for (TypeWithPrimaryKey val : currentItems) {
133+
newState.add(val);
134+
}
135+
}
136+
});
137+
}
138+
139+
@HollowPrimaryKey(fields = {"id", "name"})
140+
static class TypeWithPrimaryKey {
141+
int id;
142+
String name;
143+
String desc;
144+
145+
TypeWithPrimaryKey(int id, String name, String desc) {
146+
this.id = id;
147+
this.name = name;
148+
this.desc = desc;
149+
}
150+
}
151+
152+
}

0 commit comments

Comments
 (0)