|
24 | 24 | import static org.mockito.Mockito.doThrow; |
25 | 25 | import static org.mockito.Mockito.mock; |
26 | 26 | import static org.mockito.Mockito.spy; |
| 27 | +import static org.mockito.Mockito.verifyZeroInteractions; |
27 | 28 | import static org.mockito.Mockito.when; |
28 | 29 |
|
29 | 30 | import com.netflix.hollow.api.consumer.HollowConsumer; |
|
51 | 52 | import com.netflix.hollow.api.producer.model.CustomReferenceType; |
52 | 53 | import com.netflix.hollow.api.producer.model.HasAllTypeStates; |
53 | 54 | import com.netflix.hollow.core.HollowBlobHeader; |
| 55 | +import com.netflix.hollow.core.HollowConstants; |
54 | 56 | import com.netflix.hollow.core.read.engine.HollowBlobHeaderReader; |
| 57 | +import com.netflix.hollow.core.read.engine.HollowReadStateEngine; |
55 | 58 | import com.netflix.hollow.core.read.engine.HollowTypeReadState; |
56 | 59 | import com.netflix.hollow.core.read.engine.object.HollowObjectTypeReadState; |
57 | 60 | import com.netflix.hollow.core.schema.HollowMapSchema; |
@@ -314,6 +317,88 @@ public void testPublishAndRestore() { |
314 | 317 | assertEquals(producer.getCycleCountWithPrimaryStatus(), 1); |
315 | 318 | } |
316 | 319 |
|
| 320 | + @Test |
| 321 | + public void testRestoreFromReadStateEngine() { |
| 322 | + InMemoryBlobStore blobStore = new InMemoryBlobStore(); |
| 323 | + HollowInMemoryBlobStager blobStager = new HollowInMemoryBlobStager(); |
| 324 | + |
| 325 | + // A producer publishes an initial version. |
| 326 | + HollowProducer producer1 = HollowProducer.withPublisher(blobStore) |
| 327 | + .withBlobStager(blobStager) |
| 328 | + .build(); |
| 329 | + producer1.initializeDataModel(TestPojoV1.class); |
| 330 | + long version = producer1.runCycle(ws -> { |
| 331 | + for (int i = 1; i <= 3; i++) { |
| 332 | + ws.add(new TestPojoV1(i, i * 10)); |
| 333 | + } |
| 334 | + }); |
| 335 | + |
| 336 | + // A caller builds their own HollowConsumer and refreshes it to that version. |
| 337 | + HollowConsumer consumer = HollowConsumer.withBlobRetriever(blobStore).build(); |
| 338 | + consumer.triggerRefreshTo(version); |
| 339 | + assertEquals(version, consumer.getCurrentVersionId()); |
| 340 | + |
| 341 | + // A fresh producer restores directly from the consumer's read state engine |
| 342 | + // rather than downloading blobs itself. |
| 343 | + HollowProducer producer2 = HollowProducer.withPublisher(blobStore) |
| 344 | + .withBlobStager(blobStager) |
| 345 | + .build(); |
| 346 | + producer2.initializeDataModel(TestPojoV1.class); |
| 347 | + HollowProducer.ReadState readState = |
| 348 | + producer2.restore(consumer.getCurrentVersionId(), consumer.getStateEngine()); |
| 349 | + |
| 350 | + // The producer is restored to the expected version with the expected data. |
| 351 | + Assert.assertNotNull(readState); |
| 352 | + assertEquals(version, readState.getVersion()); |
| 353 | + HollowObjectTypeReadState typeState = |
| 354 | + (HollowObjectTypeReadState) readState.getStateEngine().getTypeState("TestPojo"); |
| 355 | + BitSet populatedOrdinals = typeState.getPopulatedOrdinals(); |
| 356 | + assertEquals(3, populatedOrdinals.cardinality()); |
| 357 | + int ordinal = populatedOrdinals.nextSetBit(0); |
| 358 | + while (ordinal != -1) { |
| 359 | + GenericHollowObject obj = new GenericHollowObject(new HollowObjectGenericDelegate(typeState), ordinal); |
| 360 | + assertEquals("v1 should equal id * 10", obj.getInt("id") * 10, obj.getInt("v1")); |
| 361 | + ordinal = populatedOrdinals.nextSetBit(ordinal + 1); |
| 362 | + } |
| 363 | + |
| 364 | + // Restore -> next delta: the cycle after restore must chain off the restored version, |
| 365 | + // so a consumer that loaded the restored version can move forward via a delta. If the |
| 366 | + // delta's "from" did not match the restored version the transition below would fail with |
| 367 | + // "Attempting to apply a delta to a state from which it was not originated!". |
| 368 | + long nextVersion = producer2.runCycle(ws -> { |
| 369 | + for (int i = 1; i <= 4; i++) { |
| 370 | + ws.add(new TestPojoV1(i, i * 10)); |
| 371 | + } |
| 372 | + }); |
| 373 | + Assert.assertNotEquals(version, nextVersion); |
| 374 | + |
| 375 | + HollowConsumer follower = HollowConsumer.withBlobRetriever(blobStore).build(); |
| 376 | + follower.triggerRefreshTo(version); |
| 377 | + assertEquals(version, follower.getCurrentVersionId()); |
| 378 | + follower.triggerRefreshTo(nextVersion); |
| 379 | + assertEquals(nextVersion, follower.getCurrentVersionId()); |
| 380 | + assertEquals(4, follower.getStateEngine().getTypeState("TestPojo") |
| 381 | + .getPopulatedOrdinals().cardinality()); |
| 382 | + } |
| 383 | + |
| 384 | + @Test |
| 385 | + public void testRestoreVersionNoneShortCircuits() { |
| 386 | + HollowProducer producer = createProducer(tmpFolder, schema); |
| 387 | + |
| 388 | + // BlobRetriever overload: a VERSION_NONE restore must short-circuit, returning null |
| 389 | + // without ever building a consumer or touching the retriever. |
| 390 | + HollowConsumer.BlobRetriever retriever = mock(HollowConsumer.BlobRetriever.class); |
| 391 | + HollowProducer.ReadState readState = producer.restore(HollowConstants.VERSION_NONE, retriever); |
| 392 | + Assert.assertNull("VERSION_NONE restore should return null", readState); |
| 393 | + verifyZeroInteractions(retriever); |
| 394 | + |
| 395 | + // ReadStateEngine overload: likewise returns null and never dereferences the engine, |
| 396 | + // so a null engine is tolerated when the version is VERSION_NONE. |
| 397 | + HollowProducer.ReadState readStateFromEngine = |
| 398 | + producer.restore(HollowConstants.VERSION_NONE, (HollowReadStateEngine) null); |
| 399 | + Assert.assertNull("VERSION_NONE restore should return null", readStateFromEngine); |
| 400 | + } |
| 401 | + |
317 | 402 | @Test |
318 | 403 | public void testHeaderPublish() throws IOException { |
319 | 404 | HollowProducer producer = createProducer(tmpFolder, schema); |
|
0 commit comments