Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent load / publish failures when missing types are defined in hash keys #679

Merged
merged 1 commit into from
Apr 22, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -232,8 +232,8 @@ static FieldPath<FieldSegment> createFieldPath(
/**
* An exception contain structured information when a field path cannot be bound.
*/
static final class FieldPathException extends IllegalArgumentException {
enum ErrorKind {
public static final class FieldPathException extends IllegalArgumentException {
public enum ErrorKind {
NOT_BINDABLE,
NOT_FOUND,
NOT_FULL,
@@ -242,7 +242,7 @@ enum ErrorKind {
;
}

final ErrorKind error;
public final ErrorKind error;
final String rootType;
final String[] segments;

Original file line number Diff line number Diff line change
@@ -16,12 +16,11 @@
*/
package com.netflix.hollow.core.read.engine.map;

import static com.netflix.hollow.core.HollowConstants.ORDINAL_NONE;

import com.netflix.hollow.api.sampling.DisabledSamplingDirector;
import com.netflix.hollow.api.sampling.HollowMapSampler;
import com.netflix.hollow.api.sampling.HollowSampler;
import com.netflix.hollow.api.sampling.HollowSamplingDirector;
import com.netflix.hollow.core.index.FieldPaths;
import com.netflix.hollow.core.index.key.HollowPrimaryKeyValueDeriver;
import com.netflix.hollow.core.memory.MemoryMode;
import com.netflix.hollow.core.memory.encoding.GapEncodedVariableLengthIntegerReader;
@@ -43,11 +42,17 @@
import com.netflix.hollow.tools.checksum.HollowChecksum;
import java.io.IOException;
import java.util.BitSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.netflix.hollow.core.HollowConstants.ORDINAL_NONE;
import static com.netflix.hollow.core.index.FieldPaths.FieldPathException.ErrorKind.NOT_BINDABLE;

/**
* A {@link HollowTypeReadState} for MAP type records.
*/
public class HollowMapTypeReadState extends HollowTypeReadState implements HollowMapTypeDataAccess {
private static final Logger LOG = Logger.getLogger(HollowMapTypeReadState.class.getName());

private final HollowMapSampler sampler;

@@ -332,9 +337,19 @@ public HollowPrimaryKeyValueDeriver getKeyDeriver() {
}

public void buildKeyDeriver() {
if(getSchema().getHashKey() != null)
this.keyDeriver = new HollowPrimaryKeyValueDeriver(getSchema().getHashKey(), getStateEngine());

if(getSchema().getHashKey() != null) {
try {
this.keyDeriver = new HollowPrimaryKeyValueDeriver(getSchema().getHashKey(), getStateEngine());
} catch (FieldPaths.FieldPathException e) {
if (e.error == NOT_BINDABLE) {
LOG.log(Level.WARNING, "Failed to create a key value deriver for " + getSchema().getHashKey() +
" because a field could not be bound to a type in the state");
} else {
throw e;
}
}
}

for(int i=0; i<shards.length; i++)
shards[i].setKeyDeriver(keyDeriver);
}
Original file line number Diff line number Diff line change
@@ -16,12 +16,11 @@
*/
package com.netflix.hollow.core.read.engine.set;

import static com.netflix.hollow.core.HollowConstants.ORDINAL_NONE;

import com.netflix.hollow.api.sampling.DisabledSamplingDirector;
import com.netflix.hollow.api.sampling.HollowSampler;
import com.netflix.hollow.api.sampling.HollowSamplingDirector;
import com.netflix.hollow.api.sampling.HollowSetSampler;
import com.netflix.hollow.core.index.FieldPaths;
import com.netflix.hollow.core.index.key.HollowPrimaryKeyValueDeriver;
import com.netflix.hollow.core.memory.MemoryMode;
import com.netflix.hollow.core.memory.encoding.GapEncodedVariableLengthIntegerReader;
@@ -44,11 +43,17 @@
import com.netflix.hollow.tools.checksum.HollowChecksum;
import java.io.IOException;
import java.util.BitSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.netflix.hollow.core.HollowConstants.ORDINAL_NONE;
import static com.netflix.hollow.core.index.FieldPaths.FieldPathException.ErrorKind.NOT_BINDABLE;

/**
* A {@link HollowTypeReadState} for OBJECT type records.
*/
public class HollowSetTypeReadState extends HollowCollectionTypeReadState implements HollowSetTypeDataAccess {
private static final Logger LOG = Logger.getLogger(HollowSetTypeReadState.class.getName());

private final HollowSetSampler sampler;

@@ -310,9 +315,19 @@ public HollowPrimaryKeyValueDeriver getKeyDeriver() {
}

public void buildKeyDeriver() {
if(getSchema().getHashKey() != null)
this.keyDeriver = new HollowPrimaryKeyValueDeriver(getSchema().getHashKey(), getStateEngine());

if(getSchema().getHashKey() != null) {
try {
this.keyDeriver = new HollowPrimaryKeyValueDeriver(getSchema().getHashKey(), getStateEngine());
} catch (FieldPaths.FieldPathException e) {
if (e.error == NOT_BINDABLE) {
LOG.log(Level.WARNING, "Failed to create a key value deriver for " + getSchema().getHashKey() +
" because a field could not be bound to a type in the state");
} else {
throw e;
}
}
}

for(int i=0;i<shards.length;i++)
shards[i].setKeyDeriver(keyDeriver);
}
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
*/
package com.netflix.hollow.core.write;

import com.netflix.hollow.core.index.FieldPaths;
import com.netflix.hollow.core.memory.ByteData;
import com.netflix.hollow.core.memory.ByteDataArray;
import com.netflix.hollow.core.memory.ThreadSafeBitSet;
@@ -26,8 +27,13 @@
import com.netflix.hollow.core.schema.HollowMapSchema;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.netflix.hollow.core.index.FieldPaths.FieldPathException.ErrorKind.NOT_BINDABLE;

public class HollowMapTypeWriteState extends HollowTypeWriteState {
private static final Logger LOG = Logger.getLogger(HollowMapTypeWriteState.class.getName());

/// statistics required for writing fixed length set data
private int bitsPerMapPointer;
@@ -212,9 +218,19 @@ public void calculateSnapshot() {

HollowWriteStateEnginePrimaryKeyHasher primaryKeyHasher = null;

if(getSchema().getHashKey() != null)
primaryKeyHasher = new HollowWriteStateEnginePrimaryKeyHasher(getSchema().getHashKey(), getStateEngine());

if(getSchema().getHashKey() != null) {
try {
primaryKeyHasher = new HollowWriteStateEnginePrimaryKeyHasher(getSchema().getHashKey(), getStateEngine());
} catch (FieldPaths.FieldPathException e) {
if (e.error == NOT_BINDABLE) {
LOG.log(Level.WARNING, "Failed to create a key hasher for " + getSchema().getHashKey() +
" because a field could not be bound to a type in the state");
} else {
throw e;
}
}
}

for(int ordinal=0;ordinal<=maxOrdinal;ordinal++) {
int shardNumber = ordinal & shardMask;
int shardOrdinal = ordinal / numShards;
@@ -378,8 +394,18 @@ private void calculateDelta(ThreadSafeBitSet fromCyclePopulated, ThreadSafeBitSe

HollowWriteStateEnginePrimaryKeyHasher primaryKeyHasher = null;

if(getSchema().getHashKey() != null)
primaryKeyHasher = new HollowWriteStateEnginePrimaryKeyHasher(getSchema().getHashKey(), getStateEngine());
if(getSchema().getHashKey() != null) {
try {
primaryKeyHasher = new HollowWriteStateEnginePrimaryKeyHasher(getSchema().getHashKey(), getStateEngine());
} catch (FieldPaths.FieldPathException e) {
if (e.error == NOT_BINDABLE) {
LOG.log(Level.WARNING, "Failed to create a key hasher for " + getSchema().getHashKey() +
" because a field could not be bound to a type in the state");
} else {
throw e;
}
}
}

for(int ordinal=0;ordinal<=maxOrdinal;ordinal++) {
int shardNumber = ordinal & shardMask;
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
*/
package com.netflix.hollow.core.write;

import com.netflix.hollow.core.index.FieldPaths;
import com.netflix.hollow.core.memory.ByteData;
import com.netflix.hollow.core.memory.ByteDataArray;
import com.netflix.hollow.core.memory.ThreadSafeBitSet;
@@ -26,8 +27,13 @@
import com.netflix.hollow.core.schema.HollowSetSchema;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import static com.netflix.hollow.core.index.FieldPaths.FieldPathException.ErrorKind.NOT_BINDABLE;

public class HollowSetTypeWriteState extends HollowTypeWriteState {
private static final Logger LOG = Logger.getLogger(HollowSetTypeWriteState.class.getName());

/// statistics required for writing fixed length set data
private int bitsPerSetPointer;
@@ -191,8 +197,18 @@ public void calculateSnapshot() {

HollowWriteStateEnginePrimaryKeyHasher primaryKeyHasher = null;

if(getSchema().getHashKey() != null)
primaryKeyHasher = new HollowWriteStateEnginePrimaryKeyHasher(getSchema().getHashKey(), getStateEngine());
if(getSchema().getHashKey() != null) {
try {
primaryKeyHasher = new HollowWriteStateEnginePrimaryKeyHasher(getSchema().getHashKey(), getStateEngine());
} catch (FieldPaths.FieldPathException e) {
if (e.error == NOT_BINDABLE) {
LOG.log(Level.WARNING, "Failed to create a key hasher for " + getSchema().getHashKey() +
" because a field could not be bound to a type in the state");
} else {
throw e;
}
}
}

for(int ordinal=0;ordinal<=maxOrdinal;ordinal++) {
int shardNumber = ordinal & shardMask;
@@ -350,8 +366,18 @@ public void calculateDelta(ThreadSafeBitSet fromCyclePopulated, ThreadSafeBitSet

HollowWriteStateEnginePrimaryKeyHasher primaryKeyHasher = null;

if(getSchema().getHashKey() != null)
primaryKeyHasher = new HollowWriteStateEnginePrimaryKeyHasher(getSchema().getHashKey(), getStateEngine());
if(getSchema().getHashKey() != null) {
try {
primaryKeyHasher = new HollowWriteStateEnginePrimaryKeyHasher(getSchema().getHashKey(), getStateEngine());
} catch (FieldPaths.FieldPathException e) {
if (e.error == NOT_BINDABLE) {
LOG.log(Level.WARNING, "Failed to create a key hasher for " + getSchema().getHashKey() +
" because a field could not be bound to a type in the state");
} else {
throw e;
}
}
}

for(int ordinal=0;ordinal<=maxOrdinal;ordinal++) {
int shardNumber = ordinal & shardMask;
Original file line number Diff line number Diff line change
@@ -0,0 +1,464 @@
package com.netflix.hollow.core.write;

import com.netflix.hollow.api.consumer.HollowConsumer;
import com.netflix.hollow.api.producer.HollowProducer;
import com.netflix.hollow.api.producer.fs.HollowInMemoryBlobStager;
import com.netflix.hollow.core.HollowDataset;
import com.netflix.hollow.core.schema.HollowObjectSchema;
import com.netflix.hollow.core.schema.HollowSchema;
import com.netflix.hollow.core.schema.SimpleHollowDataset;
import com.netflix.hollow.core.write.objectmapper.HollowHashKey;
import com.netflix.hollow.core.write.objectmapper.HollowPrimaryKey;
import com.netflix.hollow.core.write.objectmapper.HollowTypeName;
import com.netflix.hollow.test.InMemoryBlobStore;
import org.junit.Test;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static org.junit.Assert.assertEquals;

public class MissingHashKeyInWriteStateTest {
@Test
public void shouldSucceedLikeNormalIfEverythingIsWritten_setWithHashKey() {
HollowDataset dataset = SimpleHollowDataset.fromClassDefinitions(MovieWithHashKeySet.class);

InMemoryBlobStore blobStore = new InMemoryBlobStore();
HollowInMemoryBlobStager blobStager = new HollowInMemoryBlobStager();

HollowProducer p1 = HollowProducer.withPublisher(blobStore).withBlobStager(blobStager).build();
p1.initializeDataModel(dataset.getSchemas().toArray(new HollowSchema[0]));

long v1 = p1.runCycle(ws -> {
Set<Actor> actors = new HashSet<>();
actors.add(new Actor(1, "actor1"));
MovieWithHashKeySet movie = new MovieWithHashKeySet(1, "title1", actors);
ws.add(movie);
});

HollowConsumer consumer = HollowConsumer.withBlobRetriever(blobStore)
.withDoubleSnapshotConfig(new HollowConsumer.DoubleSnapshotConfig() {
@Override
public boolean allowDoubleSnapshot() {
return false;
}
@Override
public int maxDeltasBeforeDoubleSnapshot() {
return Integer.MAX_VALUE;
}
})
.build();
consumer.triggerRefreshTo(v1);

assertEquals(v1, consumer.getCurrentVersionId());
assertEquals(1, consumer.getStateEngine().getTypeState("Movie").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("MovieTitle").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("SetOfActor").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("Actor").getPopulatedOrdinals().cardinality());
}

@Test
public void shouldSucceedLikeNormalIfEverythingIsWritten_setWithoutHashKey() {
HollowDataset dataset = SimpleHollowDataset.fromClassDefinitions(MovieWithoutHashKeySet.class);

InMemoryBlobStore blobStore = new InMemoryBlobStore();
HollowInMemoryBlobStager blobStager = new HollowInMemoryBlobStager();

HollowProducer p1 = HollowProducer.withPublisher(blobStore).withBlobStager(blobStager).build();
p1.initializeDataModel(dataset.getSchemas().toArray(new HollowSchema[0]));

long v1 = p1.runCycle(ws -> {
Set<Actor> actors = new HashSet<>();
actors.add(new Actor(1, "actor1"));
MovieWithoutHashKeySet movie = new MovieWithoutHashKeySet(1, "title1", actors);
ws.add(movie);
});

HollowConsumer consumer = HollowConsumer.withBlobRetriever(blobStore)
.withDoubleSnapshotConfig(new HollowConsumer.DoubleSnapshotConfig() {
@Override
public boolean allowDoubleSnapshot() {
return false;
}
@Override
public int maxDeltasBeforeDoubleSnapshot() {
return Integer.MAX_VALUE;
}
})
.build();
consumer.triggerRefreshTo(v1);

assertEquals(v1, consumer.getCurrentVersionId());
assertEquals(1, consumer.getStateEngine().getTypeState("Movie").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("MovieTitle").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("SetOfActor").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("Actor").getPopulatedOrdinals().cardinality());
}

@Test
public void shouldNotFailProducerCycleIfSetIsNotPopulated_withHashKey() {
HollowDataset dataset = SimpleHollowDataset.fromClassDefinitions(MovieWithHashKeySet.class);

HollowSchema[] schemasWithoutActor =
dataset.getSchemas()
.stream()
.filter(schema -> !schema.getName().equals("Actor") && !schema.getName().equals("ActorName"))
.toArray(HollowSchema[]::new);

InMemoryBlobStore blobStore = new InMemoryBlobStore();
HollowInMemoryBlobStager blobStager = new HollowInMemoryBlobStager();

HollowProducer p1 = HollowProducer.withPublisher(blobStore).withBlobStager(blobStager).build();
p1.initializeDataModel(schemasWithoutActor);

long v1 = p1.runCycle(ws -> {
HollowWriteStateEngine stateEngine = ws.getStateEngine();
HollowObjectWriteRecord titleRec = new HollowObjectWriteRecord((HollowObjectSchema) dataset.getSchema("MovieTitle"));
titleRec.setString("value", "title1");
int titleOrdinal = stateEngine.add("MovieTitle", titleRec);

HollowObjectWriteRecord rec = new HollowObjectWriteRecord((HollowObjectSchema) dataset.getSchema("Movie"));
rec.setInt("id", 1);
rec.setReference("title", titleOrdinal);
rec.setNull("actors");
stateEngine.add("Movie", rec);
});

HollowConsumer consumer = HollowConsumer.withBlobRetriever(blobStore)
.withDoubleSnapshotConfig(new HollowConsumer.DoubleSnapshotConfig() {
@Override
public boolean allowDoubleSnapshot() {
return false;
}
@Override
public int maxDeltasBeforeDoubleSnapshot() {
return Integer.MAX_VALUE;
}
})
.build();
consumer.triggerRefreshTo(v1);

assertEquals(v1, consumer.getCurrentVersionId());
assertEquals(1, consumer.getStateEngine().getTypeState("Movie").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("MovieTitle").getPopulatedOrdinals().cardinality());
assertEquals(0, consumer.getStateEngine().getTypeState("SetOfActor").getPopulatedOrdinals().cardinality());
}

@Test
public void shouldNotFailProducerCycleIfSetIsNotPopulated_withoutHashKey() {
HollowDataset dataset = SimpleHollowDataset.fromClassDefinitions(MovieWithoutHashKeySet.class);

HollowSchema[] schemasWithoutActor =
dataset.getSchemas()
.stream()
.filter(schema -> !schema.getName().equals("Actor") && !schema.getName().equals("ActorName"))
.toArray(HollowSchema[]::new);

InMemoryBlobStore blobStore = new InMemoryBlobStore();
HollowInMemoryBlobStager blobStager = new HollowInMemoryBlobStager();

HollowProducer p1 = HollowProducer.withPublisher(blobStore).withBlobStager(blobStager).build();
p1.initializeDataModel(schemasWithoutActor);

long v1 = p1.runCycle(ws -> {
HollowWriteStateEngine stateEngine = ws.getStateEngine();
HollowObjectWriteRecord titleRec = new HollowObjectWriteRecord((HollowObjectSchema) dataset.getSchema("MovieTitle"));
titleRec.setString("value", "title1");
int titleOrdinal = stateEngine.add("MovieTitle", titleRec);

HollowObjectWriteRecord rec = new HollowObjectWriteRecord((HollowObjectSchema) dataset.getSchema("Movie"));
rec.setInt("id", 1);
rec.setReference("title", titleOrdinal);
rec.setNull("actors");
stateEngine.add("Movie", rec);
});

HollowConsumer consumer = HollowConsumer.withBlobRetriever(blobStore)
.withDoubleSnapshotConfig(new HollowConsumer.DoubleSnapshotConfig() {
@Override
public boolean allowDoubleSnapshot() {
return false;
}
@Override
public int maxDeltasBeforeDoubleSnapshot() {
return Integer.MAX_VALUE;
}
})
.build();
consumer.triggerRefreshTo(v1);

assertEquals(v1, consumer.getCurrentVersionId());
assertEquals(1, consumer.getStateEngine().getTypeState("Movie").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("MovieTitle").getPopulatedOrdinals().cardinality());
assertEquals(0, consumer.getStateEngine().getTypeState("SetOfActor").getPopulatedOrdinals().cardinality());
}

@Test
public void shouldSucceedLikeNormalIfEverythingIsWritten_mapWithHashKey() {
HollowDataset dataset = SimpleHollowDataset.fromClassDefinitions(MovieWithHashKeyMap.class);

InMemoryBlobStore blobStore = new InMemoryBlobStore();
HollowInMemoryBlobStager blobStager = new HollowInMemoryBlobStager();

HollowProducer p1 = HollowProducer.withPublisher(blobStore).withBlobStager(blobStager).build();
p1.initializeDataModel(dataset.getSchemas().toArray(new HollowSchema[0]));

long v1 = p1.runCycle(ws -> {
Map<Actor, Award> awardMap = new HashMap<>();
awardMap.put(new Actor(1, "actor1"), new Award(1, "award1"));
MovieWithHashKeyMap movie = new MovieWithHashKeyMap(1, "title1", awardMap);
ws.add(movie);
});

HollowConsumer consumer = HollowConsumer.withBlobRetriever(blobStore)
.withDoubleSnapshotConfig(new HollowConsumer.DoubleSnapshotConfig() {
@Override
public boolean allowDoubleSnapshot() {
return false;
}
@Override
public int maxDeltasBeforeDoubleSnapshot() {
return Integer.MAX_VALUE;
}
})
.build();
consumer.triggerRefreshTo(v1);

assertEquals(v1, consumer.getCurrentVersionId());
assertEquals(1, consumer.getStateEngine().getTypeState("Movie").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("MovieTitle").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("MapOfActorToAward").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("Actor").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("Award").getPopulatedOrdinals().cardinality());
}

@Test
public void shouldSucceedLikeNormalIfEverythingIsWritten_mapWithoutHashKey() {
HollowDataset dataset = SimpleHollowDataset.fromClassDefinitions(MovieWithoutHashKeyMap.class);

InMemoryBlobStore blobStore = new InMemoryBlobStore();
HollowInMemoryBlobStager blobStager = new HollowInMemoryBlobStager();

HollowProducer p1 = HollowProducer.withPublisher(blobStore).withBlobStager(blobStager).build();
p1.initializeDataModel(dataset.getSchemas().toArray(new HollowSchema[0]));

long v1 = p1.runCycle(ws -> {
Map<Actor, Award> awardMap = new HashMap<>();
awardMap.put(new Actor(1, "actor1"), new Award(1, "award1"));
MovieWithoutHashKeyMap movie = new MovieWithoutHashKeyMap(1, "title1", awardMap);
ws.add(movie);
});

HollowConsumer consumer = HollowConsumer.withBlobRetriever(blobStore)
.withDoubleSnapshotConfig(new HollowConsumer.DoubleSnapshotConfig() {
@Override
public boolean allowDoubleSnapshot() {
return false;
}
@Override
public int maxDeltasBeforeDoubleSnapshot() {
return Integer.MAX_VALUE;
}
})
.build();
consumer.triggerRefreshTo(v1);

assertEquals(v1, consumer.getCurrentVersionId());
assertEquals(1, consumer.getStateEngine().getTypeState("Movie").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("MovieTitle").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("MapOfActorToAward").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("Actor").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("Award").getPopulatedOrdinals().cardinality());
}

@Test
public void shouldNotFailProducerCycleIfMapIsNotPopulated_withHashKey() {
HollowDataset dataset = SimpleHollowDataset.fromClassDefinitions(MovieWithHashKeyMap.class);

HollowSchema[] schemasWithoutActorOrAward =
dataset.getSchemas()
.stream()
.filter(schema ->
!schema.getName().equals("Actor") &&
!schema.getName().equals("ActorName") &&
!schema.getName().equals("Award") &&
!schema.getName().equals("AwardName"))
.toArray(HollowSchema[]::new);

InMemoryBlobStore blobStore = new InMemoryBlobStore();
HollowInMemoryBlobStager blobStager = new HollowInMemoryBlobStager();

HollowProducer p1 = HollowProducer.withPublisher(blobStore).withBlobStager(blobStager).build();
p1.initializeDataModel(schemasWithoutActorOrAward);

long v1 = p1.runCycle(ws -> {
HollowWriteStateEngine stateEngine = ws.getStateEngine();
HollowObjectWriteRecord titleRec = new HollowObjectWriteRecord((HollowObjectSchema) dataset.getSchema("MovieTitle"));
titleRec.setString("value", "title1");
int titleOrdinal = stateEngine.add("MovieTitle", titleRec);

HollowObjectWriteRecord rec = new HollowObjectWriteRecord((HollowObjectSchema) dataset.getSchema("Movie"));
rec.setInt("id", 1);
rec.setReference("title", titleOrdinal);
rec.setNull("awardMap");
stateEngine.add("Movie", rec);
});

HollowConsumer consumer = HollowConsumer.withBlobRetriever(blobStore)
.withDoubleSnapshotConfig(new HollowConsumer.DoubleSnapshotConfig() {
@Override
public boolean allowDoubleSnapshot() {
return false;
}
@Override
public int maxDeltasBeforeDoubleSnapshot() {
return Integer.MAX_VALUE;
}
})
.build();
consumer.triggerRefreshTo(v1);

assertEquals(v1, consumer.getCurrentVersionId());
assertEquals(1, consumer.getStateEngine().getTypeState("Movie").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("MovieTitle").getPopulatedOrdinals().cardinality());
assertEquals(0, consumer.getStateEngine().getTypeState("MapOfActorToAward").getPopulatedOrdinals().cardinality());
}

@Test
public void shouldNotFailProducerCycleIfMapIsNotPopulated_withoutHashKey() {
HollowDataset dataset = SimpleHollowDataset.fromClassDefinitions(MovieWithoutHashKeyMap.class);
HollowSchema[] schemasWithoutActorOrAward =
dataset.getSchemas()
.stream()
.filter(schema ->
!schema.getName().equals("Actor") &&
!schema.getName().equals("ActorName") &&
!schema.getName().equals("Award") &&
!schema.getName().equals("AwardName"))
.toArray(HollowSchema[]::new);

InMemoryBlobStore blobStore = new InMemoryBlobStore();
HollowInMemoryBlobStager blobStager = new HollowInMemoryBlobStager();

HollowProducer p1 = HollowProducer.withPublisher(blobStore).withBlobStager(blobStager).build();
p1.initializeDataModel(schemasWithoutActorOrAward);

long v1 = p1.runCycle(ws -> {
HollowWriteStateEngine stateEngine = ws.getStateEngine();
HollowObjectWriteRecord titleRec = new HollowObjectWriteRecord((HollowObjectSchema) dataset.getSchema("MovieTitle"));
titleRec.setString("value", "title1");
int titleOrdinal = stateEngine.add("MovieTitle", titleRec);

HollowObjectWriteRecord rec = new HollowObjectWriteRecord((HollowObjectSchema) dataset.getSchema("Movie"));
rec.setInt("id", 1);
rec.setReference("title", titleOrdinal);
rec.setNull("awardMap");
stateEngine.add("Movie", rec);
});

HollowConsumer consumer = HollowConsumer.withBlobRetriever(blobStore)
.withDoubleSnapshotConfig(new HollowConsumer.DoubleSnapshotConfig() {
@Override
public boolean allowDoubleSnapshot() {
return false;
}
@Override
public int maxDeltasBeforeDoubleSnapshot() {
return Integer.MAX_VALUE;
}
})
.build();
consumer.triggerRefreshTo(v1);

assertEquals(v1, consumer.getCurrentVersionId());
assertEquals(1, consumer.getStateEngine().getTypeState("Movie").getPopulatedOrdinals().cardinality());
assertEquals(1, consumer.getStateEngine().getTypeState("MovieTitle").getPopulatedOrdinals().cardinality());
assertEquals(0, consumer.getStateEngine().getTypeState("MapOfActorToAward").getPopulatedOrdinals().cardinality());
}

@HollowTypeName(name="Movie")
@HollowPrimaryKey(fields="id")
private static class MovieWithoutHashKeySet {
int id;
@HollowTypeName(name="MovieTitle")
String title;
Set<Actor> actors;

private MovieWithoutHashKeySet(int id, String title, Set<Actor> actors) {
this.id = id;
this.title = title;
this.actors = actors;
}
}

@HollowTypeName(name="Movie")
@HollowPrimaryKey(fields="id")
private static class MovieWithHashKeySet {
int id;
@HollowTypeName(name="MovieTitle")
String title;
@HollowHashKey(fields="id")
Set<Actor> actors;

private MovieWithHashKeySet(int id, String title, Set<Actor> actors) {
this.id = id;
this.title = title;
this.actors = actors;
}
}

@HollowTypeName(name="Movie")
@HollowPrimaryKey(fields="id")
private static class MovieWithoutHashKeyMap {
int id;
@HollowTypeName(name="MovieTitle")
String title;
Map<Actor, Award> awardMap;

private MovieWithoutHashKeyMap(int id, String title, Map<Actor, Award> awardMap) {
this.id = id;
this.title = title;
this.awardMap = awardMap;
}
}

@HollowTypeName(name="Movie")
@HollowPrimaryKey(fields="id")
private static class MovieWithHashKeyMap {
int id;
@HollowTypeName(name="MovieTitle")
String title;
@HollowHashKey(fields="id")
Map<Actor, Award> awardMap;

private MovieWithHashKeyMap(int id, String title, Map<Actor, Award> awardMap) {
this.id = id;
this.title = title;
this.awardMap = awardMap;
}
}

private static class Actor {
int id;
@HollowTypeName(name="ActorName")
String name;

private Actor(int id, String name) {
this.id = id;
this.name = name;
}
}

private static class Award {
int id;
@HollowTypeName(name="AwardName")
String name;

private Award(int id, String name) {
this.id = id;
this.name = name;
}
}
}