|
18 | 18 | */ |
19 | 19 | package org.apache.pulsar.io.jdbc; |
20 | 20 |
|
| 21 | +import static org.mockito.ArgumentMatchers.any; |
21 | 22 | import static org.mockito.Mockito.doAnswer; |
22 | 23 | import static org.mockito.Mockito.mock; |
| 24 | +import static org.mockito.Mockito.verify; |
23 | 25 | import static org.mockito.Mockito.when; |
24 | 26 | import com.google.common.collect.ImmutableMap; |
25 | 27 | import com.google.common.collect.Maps; |
|
56 | 58 | import org.apache.pulsar.common.schema.SchemaType; |
57 | 59 | import org.apache.pulsar.functions.api.Record; |
58 | 60 | import org.apache.pulsar.functions.source.PulsarRecord; |
| 61 | +import org.apache.pulsar.io.core.SinkContext; |
59 | 62 | import org.awaitility.Awaitility; |
60 | 63 | import org.testng.Assert; |
61 | 64 | import org.testng.annotations.AfterMethod; |
@@ -860,6 +863,50 @@ public void testNullValueAction(NullValueActionTestConfig config) throws Excepti |
860 | 863 | } |
861 | 864 | } |
862 | 865 |
|
| 866 | + /** |
| 867 | + * Test that fatal() is called when an unrecoverable exception occurs during flush. |
| 868 | + * This verifies the PIP-297 implementation for proper termination of the sink. |
| 869 | + */ |
| 870 | + @Test |
| 871 | + public void testFatalCalledOnFlushException() throws Exception { |
| 872 | + jdbcSink.close(); |
| 873 | + jdbcSink = null; |
| 874 | + |
| 875 | + String jdbcUrl = sqliteUtils.sqliteUri(); |
| 876 | + Map<String, Object> conf = Maps.newHashMap(); |
| 877 | + conf.put("jdbcUrl", jdbcUrl); |
| 878 | + conf.put("tableName", "nonexistent_table"); // This will cause an exception on flush |
| 879 | + conf.put("key", "field3"); |
| 880 | + conf.put("nonKey", "field1,field2"); |
| 881 | + conf.put("batchSize", 1); |
| 882 | + |
| 883 | + SinkContext mockSinkContext = mock(SinkContext.class); |
| 884 | + AtomicReference<Throwable> fatalException = new AtomicReference<>(); |
| 885 | + doAnswer(invocation -> { |
| 886 | + fatalException.set(invocation.getArgument(0)); |
| 887 | + return null; |
| 888 | + }).when(mockSinkContext).fatal(any(Throwable.class)); |
| 889 | + |
| 890 | + SqliteJdbcAutoSchemaSink sinkWithContext = new SqliteJdbcAutoSchemaSink(); |
| 891 | + try { |
| 892 | + sinkWithContext.open(conf, mockSinkContext); |
| 893 | + |
| 894 | + Foo insertObj = new Foo("f1", "f2", 1); |
| 895 | + Map<String, String> props = Maps.newHashMap(); |
| 896 | + props.put("ACTION", "INSERT"); |
| 897 | + CompletableFuture<Boolean> future = new CompletableFuture<>(); |
| 898 | + sinkWithContext.write(createMockFooRecord(insertObj, props, future)); |
| 899 | + |
| 900 | + // Wait for the flush to complete and fail |
| 901 | + Awaitility.await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { |
| 902 | + verify(mockSinkContext).fatal(any(Throwable.class)); |
| 903 | + Assert.assertNotNull(fatalException.get()); |
| 904 | + }); |
| 905 | + } finally { |
| 906 | + sinkWithContext.close(); |
| 907 | + } |
| 908 | + } |
| 909 | + |
863 | 910 | @SuppressWarnings("unchecked") |
864 | 911 | private Record<GenericObject> createMockFooRecord(Foo record, Map<String, String> actionProperties, |
865 | 912 | CompletableFuture<Boolean> future) { |
|
0 commit comments