Skip to content

Commit 4ed44f9

Browse files
committed
Add streaming, failure and Managed Iceberg end-to-end test coverage
- Managed Iceberg write through OpenLineageRunner: asserts the START event carries the physical-location dataset with the catalog TABLE symlink after a real write to a Hadoop-catalog table. - Unbounded pipeline with non-blocking run: asserts periodic RUNNING heartbeats at the configured tracking interval and the CANCELLED to ABORT terminal mapping on cancel(). - Failing pipeline: asserts FAIL is emitted with an errorMessage run facet carrying the user exception. Addresses #39427.
1 parent 8ba0bee commit 4ed44f9

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

sdks/java/extensions/openlineage/src/test/java/org/apache/beam/sdk/extensions/openlineage/OpenLineageRunnerTest.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ public void tearDown() {
7272
OpenLineageContext.resetForTests();
7373
}
7474

75+
private static class ThrowingFn extends DoFn<Integer, Integer> {
76+
@ProcessElement
77+
public void processElement() {
78+
throw new IllegalStateException("boom");
79+
}
80+
}
81+
7582
/** Mirrors the runtime Lineage calls IO connectors make (e.g. PubsubIO.java). */
7683
private static class ReportingFn extends DoFn<Integer, Integer> {
7784
private transient boolean reported;
@@ -140,6 +147,114 @@ public void testParentRunFacetAttachedWhenFullyConfigured() throws Exception {
140147
assertEquals("airflow_namespace", parent.getJob().getNamespace());
141148
}
142149

150+
@Test
151+
public void testManagedIcebergWriteEmitsDatasetWithSymlink() throws Exception {
152+
String warehouse = "file://" + temporaryFolder.newFolder("e2e-warehouse").getAbsolutePath();
153+
org.apache.iceberg.catalog.TableIdentifier tableId =
154+
org.apache.iceberg.catalog.TableIdentifier.parse("demo.orders");
155+
try (org.apache.iceberg.hadoop.HadoopCatalog catalog =
156+
new org.apache.iceberg.hadoop.HadoopCatalog(
157+
new org.apache.hadoop.conf.Configuration(), warehouse)) {
158+
catalog.createTable(
159+
tableId,
160+
new org.apache.iceberg.Schema(
161+
org.apache.iceberg.types.Types.NestedField.required(
162+
1, "id", org.apache.iceberg.types.Types.LongType.get()),
163+
org.apache.iceberg.types.Types.NestedField.required(
164+
2, "name", org.apache.iceberg.types.Types.StringType.get())));
165+
}
166+
java.util.Map<String, Object> config = new java.util.HashMap<>();
167+
config.put("table", "demo.orders");
168+
config.put("catalog_name", "local");
169+
java.util.Map<String, String> catalogProps = new java.util.HashMap<>();
170+
catalogProps.put("type", "hadoop");
171+
catalogProps.put("warehouse", warehouse);
172+
config.put("catalog_properties", catalogProps);
173+
174+
PipelineOptions options = PipelineOptionsFactory.create();
175+
options.setRunner(OpenLineageRunner.class);
176+
org.apache.beam.sdk.schemas.Schema beamSchema =
177+
org.apache.beam.sdk.schemas.Schema.builder()
178+
.addInt64Field("id")
179+
.addStringField("name")
180+
.build();
181+
182+
Pipeline pipeline = Pipeline.create(options);
183+
pipeline
184+
.apply(
185+
Create.of(
186+
org.apache.beam.sdk.values.Row.withSchema(beamSchema)
187+
.addValues(1L, "laptop")
188+
.build())
189+
.withRowSchema(beamSchema))
190+
.apply(
191+
org.apache.beam.sdk.managed.Managed.write(org.apache.beam.sdk.managed.Managed.ICEBERG)
192+
.withConfig(config));
193+
pipeline.run().waitUntilFinish();
194+
195+
List<OpenLineage.RunEvent> events = awaitTerminalEvent();
196+
// The graph-extracted Iceberg dataset must be on the START event already.
197+
OpenLineage.OutputDataset output = events.get(0).getOutputs().get(0);
198+
assertEquals("file", output.getNamespace());
199+
assertTrue(output.getName().endsWith("/e2e-warehouse/demo/orders"));
200+
OpenLineage.SymlinksDatasetFacetIdentifiers symlink =
201+
output.getFacets().getSymlinks().getIdentifiers().get(0);
202+
assertEquals("demo.orders", symlink.getName());
203+
assertEquals("TABLE", symlink.getType());
204+
}
205+
206+
@Test
207+
public void testStreamingPipelineEmitsRunningHeartbeatsAndAbortOnCancel() throws Exception {
208+
PipelineOptions options = PipelineOptionsFactory.fromArgs("--blockOnRun=false").create();
209+
options.setRunner(OpenLineageRunner.class);
210+
options.as(OpenLineagePipelineOptions.class).setOpenLineageTrackingIntervalInSeconds(1);
211+
212+
Pipeline pipeline = Pipeline.create(options);
213+
pipeline.apply(org.apache.beam.sdk.io.GenerateSequence.from(0));
214+
PipelineResult result = pipeline.run();
215+
216+
// The tracker must emit periodic RUNNING events while the job runs...
217+
awaitEventCount(OpenLineage.RunEvent.EventType.RUNNING, 2);
218+
// ...and CANCELLED must map to ABORT, per the Flink integration's terminal mapping.
219+
result.cancel();
220+
awaitEventCount(OpenLineage.RunEvent.EventType.ABORT, 1);
221+
}
222+
223+
@Test
224+
public void testFailingPipelineEmitsFailWithErrorMessageFacet() throws Exception {
225+
PipelineOptions options = PipelineOptionsFactory.create();
226+
options.setRunner(OpenLineageRunner.class);
227+
228+
Pipeline pipeline = Pipeline.create(options);
229+
pipeline.apply(Create.of(1)).apply(ParDo.of(new ThrowingFn()));
230+
try {
231+
pipeline.run().waitUntilFinish();
232+
throw new AssertionError("pipeline should have failed");
233+
} catch (RuntimeException expected) {
234+
// expected
235+
}
236+
237+
List<OpenLineage.RunEvent> events = awaitEventCount(OpenLineage.RunEvent.EventType.FAIL, 1);
238+
OpenLineage.RunEvent fail = events.get(events.size() - 1);
239+
String message = fail.getRun().getFacets().getErrorMessage().getMessage();
240+
assertTrue("errorMessage was: " + message, message.contains("boom"));
241+
assertEquals("JAVA", fail.getRun().getFacets().getErrorMessage().getProgrammingLanguage());
242+
}
243+
244+
private List<OpenLineage.RunEvent> awaitEventCount(
245+
OpenLineage.RunEvent.EventType type, int minCount) throws Exception {
246+
long deadline = System.currentTimeMillis() + 30_000;
247+
List<OpenLineage.RunEvent> events = readEvents();
248+
while (System.currentTimeMillis() < deadline) {
249+
events = readEvents();
250+
if (events.stream().filter(e -> e.getEventType() == type).count() >= minCount) {
251+
return events;
252+
}
253+
Thread.sleep(250);
254+
}
255+
throw new AssertionError("Expected " + minCount + " " + type + " events; got " + events);
256+
}
257+
143258
@Test
144259
public void testDisabledOptionSuppressesAllEvents() throws Exception {
145260
PipelineOptions options = PipelineOptionsFactory.create();

0 commit comments

Comments
 (0)