|
24 | 24 | import com.apple.foundationdb.async.MoreAsyncUtil;
|
25 | 25 | import com.apple.foundationdb.record.cursors.FilterCursor;
|
26 | 26 | import com.apple.foundationdb.record.cursors.FirableCursor;
|
27 |
| -import com.apple.foundationdb.record.cursors.FutureCursor; |
28 | 27 | import com.apple.foundationdb.record.cursors.LazyCursor;
|
29 | 28 | import com.apple.foundationdb.record.cursors.MapResultCursor;
|
30 | 29 | import com.apple.foundationdb.record.cursors.RowLimitedCursor;
|
|
63 | 62 | import java.util.TimerTask;
|
64 | 63 | import java.util.concurrent.CancellationException;
|
65 | 64 | import java.util.concurrent.CompletableFuture;
|
| 65 | +import java.util.concurrent.CompletionException; |
66 | 66 | import java.util.concurrent.ExecutionException;
|
67 | 67 | import java.util.concurrent.Executor;
|
68 | 68 | import java.util.concurrent.ScheduledExecutorService;
|
|
81 | 81 | import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
82 | 82 | import static org.hamcrest.Matchers.instanceOf;
|
83 | 83 | import static org.hamcrest.Matchers.is;
|
| 84 | +import static org.hamcrest.Matchers.notNullValue; |
84 | 85 | import static org.hamcrest.Matchers.oneOf;
|
85 | 86 | import static org.junit.jupiter.api.Assertions.assertEquals;
|
86 | 87 | import static org.junit.jupiter.api.Assertions.assertFalse;
|
87 | 88 | import static org.junit.jupiter.api.Assertions.assertNotNull;
|
88 | 89 | import static org.junit.jupiter.api.Assertions.assertNull;
|
| 90 | +import static org.junit.jupiter.api.Assertions.assertSame; |
89 | 91 | import static org.junit.jupiter.api.Assertions.assertThrows;
|
90 | 92 | import static org.junit.jupiter.api.Assertions.assertTrue;
|
91 | 93 | import static org.junit.jupiter.api.Assertions.fail;
|
@@ -1313,7 +1315,7 @@ private static RecordCursor<Integer> mapPipelinedCursorToClose(int iteration, Co
|
1313 | 1315 | private static RecordCursor<String> singletonFlatMapPipelinedCursorToClose(int iteration, CompletableFuture<Void> signal) {
|
1314 | 1316 | return RecordCursor.flatMapPipelined(
|
1315 | 1317 | outerContinuation -> RecordCursor.fromList(EXECUTOR, IntStream.range(0, iteration % 199).boxed().collect(Collectors.toList()), outerContinuation),
|
1316 |
| - (outerValue, innerContinuation) -> new FutureCursor<>(EXECUTOR, signal.thenApplyAsync(ignore -> String.valueOf(outerValue), EXECUTOR)), |
| 1318 | + (outerValue, innerContinuation) -> RecordCursor.fromFuture(EXECUTOR, signal.thenApplyAsync(ignore -> String.valueOf(outerValue), EXECUTOR), innerContinuation), |
1317 | 1319 | null,
|
1318 | 1320 | null,
|
1319 | 1321 | iteration % 19 + 2
|
@@ -1393,4 +1395,113 @@ void pipelinedCursorAfterClosing(@Nonnull BiFunction<Integer, CompletableFuture<
|
1393 | 1395 | assertThat(executionException.getCause(), Matchers.instanceOf(CancellationException.class));
|
1394 | 1396 | }
|
1395 | 1397 | }
|
| 1398 | + |
| 1399 | + @Test |
| 1400 | + void futureCursorTest() { |
| 1401 | + CompletableFuture<Integer> future = new CompletableFuture<>(); |
| 1402 | + final RecordCursorContinuation continuation; |
| 1403 | + try (RecordCursor<Integer> fromFuture = RecordCursor.fromFuture(EXECUTOR, future, null)) { |
| 1404 | + assertSame(EXECUTOR, fromFuture.getExecutor()); |
| 1405 | + future.complete(42); |
| 1406 | + RecordCursorResult<Integer> result = fromFuture.getNext(); |
| 1407 | + assertTrue(result.hasNext(), "first result from future should have value"); |
| 1408 | + assertEquals(42, result.get()); |
| 1409 | + assertFalse(result.getContinuation().isEnd()); |
| 1410 | + continuation = result.getContinuation(); |
| 1411 | + |
| 1412 | + result = fromFuture.getNext(); |
| 1413 | + assertFalse(result.hasNext(), "second result from future should have value"); |
| 1414 | + assertEquals(RecordCursor.NoNextReason.SOURCE_EXHAUSTED, result.getNoNextReason()); |
| 1415 | + assertTrue(result.getContinuation().isEnd()); |
| 1416 | + } |
| 1417 | + |
| 1418 | + CompletableFuture<Integer> secondFuture = new CompletableFuture<>(); |
| 1419 | + try (RecordCursor<Integer> fromFuture = RecordCursor.fromFuture(EXECUTOR, secondFuture, continuation.toBytes())) { |
| 1420 | + assertSame(EXECUTOR, fromFuture.getExecutor()); |
| 1421 | + |
| 1422 | + RecordCursorResult<Integer> result = fromFuture.getNext(); |
| 1423 | + assertFalse(result.hasNext(), "future should not have value when resuming from a continuation"); |
| 1424 | + assertEquals(RecordCursor.NoNextReason.SOURCE_EXHAUSTED, result.getNoNextReason()); |
| 1425 | + assertTrue(result.getContinuation().isEnd()); |
| 1426 | + } |
| 1427 | + } |
| 1428 | + |
| 1429 | + @Test |
| 1430 | + void futureCursorFromSupplierTest() { |
| 1431 | + CompletableFuture<Integer> future = new CompletableFuture<>(); |
| 1432 | + final RecordCursorContinuation continuation; |
| 1433 | + try (RecordCursor<Integer> fromFuture = RecordCursor.fromFuture(EXECUTOR, () -> future, null)) { |
| 1434 | + assertSame(EXECUTOR, fromFuture.getExecutor()); |
| 1435 | + future.complete(1066); |
| 1436 | + RecordCursorResult<Integer> result = fromFuture.getNext(); |
| 1437 | + assertTrue(result.hasNext(), "first result from future should have value"); |
| 1438 | + assertEquals(1066, result.get()); |
| 1439 | + assertFalse(result.getContinuation().isEnd()); |
| 1440 | + continuation = result.getContinuation(); |
| 1441 | + |
| 1442 | + result = fromFuture.getNext(); |
| 1443 | + assertFalse(result.hasNext(), "second result from future should have value"); |
| 1444 | + assertEquals(RecordCursor.NoNextReason.SOURCE_EXHAUSTED, result.getNoNextReason()); |
| 1445 | + assertTrue(result.getContinuation().isEnd()); |
| 1446 | + } |
| 1447 | + |
| 1448 | + try (RecordCursor<Integer> fromFuture = RecordCursor.fromFuture(EXECUTOR, () -> fail("should not be run"), continuation.toBytes())) { |
| 1449 | + assertSame(EXECUTOR, fromFuture.getExecutor()); |
| 1450 | + |
| 1451 | + RecordCursorResult<Integer> result = fromFuture.getNext(); |
| 1452 | + assertFalse(result.hasNext(), "future should not have value when resuming from a continuation"); |
| 1453 | + assertEquals(RecordCursor.NoNextReason.SOURCE_EXHAUSTED, result.getNoNextReason()); |
| 1454 | + assertTrue(result.getContinuation().isEnd()); |
| 1455 | + } |
| 1456 | + } |
| 1457 | + |
| 1458 | + @Test |
| 1459 | + void futureCursorCompletesWhenUnderlyingCompletes() throws Exception { |
| 1460 | + final CompletableFuture<Integer> future = new CompletableFuture<>(); |
| 1461 | + final RecordCursorContinuation continuation; |
| 1462 | + try (RecordCursor<Integer> fromFuture = RecordCursor.fromFuture(EXECUTOR, future, null)) { |
| 1463 | + assertSame(EXECUTOR, fromFuture.getExecutor()); |
| 1464 | + CompletableFuture<RecordCursorResult<Integer>> resultFuture = fromFuture.onNext(); |
| 1465 | + assertFalse(resultFuture.isDone(), "result should not be done until underlying completes"); |
| 1466 | + future.complete(1819); |
| 1467 | + |
| 1468 | + // The previous result future should complete quickly. The timeout here is |
| 1469 | + // a bit of a hedge, but as currently written, the resultFuture should actually complete |
| 1470 | + // on this thread, and so the resultFuture should already be completed |
| 1471 | + // by now. But that's not necessarily part of the contract of the cursor, so |
| 1472 | + // instead, just assert here that we complete in a reasonable amount of time |
| 1473 | + RecordCursorResult<Integer> result = resultFuture.get(1, TimeUnit.SECONDS); |
| 1474 | + assertTrue(result.hasNext()); |
| 1475 | + assertEquals(1819, result.get()); |
| 1476 | + continuation = result.getContinuation(); |
| 1477 | + assertFalse(continuation.isEnd()); |
| 1478 | + assertFalse(continuation.toByteString().isEmpty()); |
| 1479 | + assertThat(continuation.toBytes(), notNullValue()); |
| 1480 | + } |
| 1481 | + |
| 1482 | + try (RecordCursor<Integer> fromFuture = RecordCursor.fromFuture(EXECUTOR, () -> fail("should not run"), continuation.toBytes())) { |
| 1483 | + assertSame(EXECUTOR, fromFuture.getExecutor()); |
| 1484 | + CompletableFuture<RecordCursorResult<Integer>> resultFuture = fromFuture.onNext(); |
| 1485 | + assertTrue(resultFuture.isDone(), "empty result should not need to wait"); |
| 1486 | + RecordCursorResult<Integer> result = resultFuture.get(); |
| 1487 | + assertFalse(result.hasNext()); |
| 1488 | + assertEquals(RecordCursor.NoNextReason.SOURCE_EXHAUSTED, result.getNoNextReason()); |
| 1489 | + assertTrue(result.getContinuation().isEnd()); |
| 1490 | + } |
| 1491 | + } |
| 1492 | + |
| 1493 | + @Test |
| 1494 | + void futureCursorPropagatesError() { |
| 1495 | + final CompletableFuture<Integer> future = new CompletableFuture<>(); |
| 1496 | + try (RecordCursor<Integer> fromFuture = RecordCursor.fromFuture(EXECUTOR, future, null)) { |
| 1497 | + assertSame(EXECUTOR, fromFuture.getExecutor()); |
| 1498 | + CompletableFuture<RecordCursorResult<Integer>> resultFuture = fromFuture.onNext(); |
| 1499 | + assertFalse(resultFuture.isDone(), "result should not be done until underlying completes"); |
| 1500 | + |
| 1501 | + final RecordCoreException error = new RecordCoreException("test error"); |
| 1502 | + future.completeExceptionally(error); |
| 1503 | + CompletionException futureError = assertThrows(CompletionException.class, future::join); |
| 1504 | + assertSame(error, futureError.getCause(), "result should complete exception while propagating the cause"); |
| 1505 | + } |
| 1506 | + } |
1396 | 1507 | }
|
0 commit comments