Repository navigation
Conversation
beam run FILE runs a Beam YAML pipeline file like python -m apache_beam.yaml.main --yaml_pipeline_file=FILE, passing every other flag through as a pipeline option. It reports errors without tracebacks (unless --verbose), uses documented exit codes, cancels the pipeline on Ctrl-C, and names the runner that ran the pipeline.
There was a problem hiding this comment.
📋 Review Summary
This PR introduces an experimental beam CLI entry point (apache_beam.cli.main) that supports executing Python, Java (JAR), and YAML pipelines via a beam run subcommand with automatic pipeline type detection and unified option passing. The implementation is well-structured, cleanly separates argument parsing from execution logic, and includes good unit test coverage.
🔍 General Feedback
- The CLI architecture and pipeline type inference logic are clean and extensible for future subcommands.
- There are a couple of bugs in the Java JAR execution path: non-portable runners like
DataflowRunnerdo not implementrun_portable_pipelinedirectly (expectingrun_pipeline), and passing--experimentson the command line will crash due to missing default initialization forknown_args.experiments.
💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.
💬 Inline Findings
-
sdks/python/apache_beam/cli/main.py(line 227): 🟠 High:known_args.experimentsis never registered onparser, sogetattr(known_args, 'experiments', None)will always returnNoneand--experimentsflags will stay inpipeline_args. However, if--experimentsis ever added toparserorknown_argsis populated withNone,known_args.experimentswould beNoneand.append()on line 229 would raiseAttributeError: 'NoneType' object has no attribute 'append'. More importantly, because--experimentsis inpipeline_args, passing['--experiments=jar_packages=...'] + pipeline_argstoPipelineOptionswhenpipeline_argsalso contains--experiments=...can override or conflict depending on howDebugOptionsparses multiple--experimentsflags. Instead, configureDebugOptionsdirectly on the constructedPipelineOptionsobject (or initializeknown_args.experimentsproperly). -
sdks/python/apache_beam/cli/main.py(line 258): 🔴 Critical:PipelineRunner.run_portable_pipelineraisesNotImplementedErroron runners that do not inherit fromPortableRunner, notablyDataflowRunner(which implementsrun_pipeline(pipeline, options, pipeline_proto=...)instead ofrun_portable_pipeline) andFnApiRunner/DirectRunner. Callingrunner.run_portable_pipeline(...)directly will fail withNotImplementedErrorwhen running a Java JAR on Dataflow or DirectRunner. Usingbeam.Pipeline.from_runner_api(pipeline_proto, runner, pipeline_options).run()(orrunner.run_pipeline(None, pipeline_options, pipeline_proto=pipeline_proto)for Dataflow) will properly route through the runner's execution path. -
sdks/python/apache_beam/cli/main.py(line 122): 🟡 Medium: Ifsys.argv[1]is an option flag other than-h/--help(for example,beam --runner=DirectRunner pipeline.pyorbeam --version), this condition treats--runner=DirectRunneras the subcommand name, skips inserting'run', and fails with aninvalid choiceerror fromargparse. Checkingnot sys.argv[1].startswith('-')would only treat non-flag arguments as subcommand candidates while allowing top-level flags or defaulting torun.
if (len(sys.argv) > 1 and sys.argv[1] not in KNOWN_COMMANDS and
|
Assigning reviewers: R: @damccorm for label python. Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
| pipeline = beam.Pipeline(options=options, display_data=display_data) | ||
| constructor(pipeline) |
There was a problem hiding this comment.
Calling beam.Pipeline(...) and pipeline.run() directly without entering Pipeline.__enter__ skips the two context managers set up in Pipeline.__enter__:
subprocess_server.JavaJarServer.beam_services(options.view_as(CrossLanguageOptions).beam_services)subprocess_server.SubprocessServer.cache_subprocesses()
For Beam YAML pipelines that use Java expansion services (e.g. Sql, BigQuery, Kafka, JDBC, Iceberg), SubprocessServer.cache_subprocesses() is required so the expansion service subprocess stays alive between constructor(pipeline) and pipeline.run() (when artifacts are retrieved/staged from the expansion service) and is shared across transforms.
You can still keep the separate try/except blocks for build vs. run by wrapping both in those context managers (or calling pipeline.__enter__() with try ... finally: pipeline._extra_context.close()). Could you also add a unit test verifying that these context managers are active across constructor(pipeline) and pipeline.run()?
There was a problem hiding this comment.
Good catch, thank you! Updated _run in af1e34b to call pipeline.enter() before constructor(pipeline) and close pipeline._extra_context.close() in a finally block spanning both constructor(pipeline) and pipeline.run(), and added test_pipeline_context_active_across_build_and_run verifying that SubprocessServer.cache_subprocesses() and JavaJarServer.beam_services(...) stay active across both steps and are cleaned up on completion or build failure.
| result = pipeline.run() | ||
| try: | ||
| state = result.wait_until_finish() | ||
| except KeyboardInterrupt: | ||
| # main() reports the interruption after the pipeline is cancelled. | ||
| _cancel(result) | ||
| raise | ||
| except Exception as e: # pylint: disable=broad-except | ||
| _maybe_print_traceback(verbose) | ||
| more = '' if verbose else '\n rerun with --verbose for the traceback' | ||
| raise _CliError( | ||
| f'pipeline failed: {_summarize(e)}{more}', | ||
| EXIT_PIPELINE_FAILED) from None | ||
| if state != PipelineState.DONE: | ||
| raise _CliError(f'pipeline finished in state {state}', EXIT_PIPELINE_FAILED) |
There was a problem hiding this comment.
In Pipeline.__exit__, result.wait_until_finish() is skipped when options.view_as(StandardOptions).no_wait_until_finish is set (for example, when launching a streaming or async job on Dataflow). Should we check options.view_as(StandardOptions).no_wait_until_finish here before calling result.wait_until_finish() and checking state != PipelineState.DONE? Please also add a test covering --no_wait_until_finish.
There was a problem hiding this comment.
Done in af1e34b — _run now checks options.view_as(StandardOptions).no_wait_until_finish before calling result.wait_until_finish() / checking PipelineState.DONE, prints Submitted ... (not waiting for completion)., and covers this in test_no_wait_until_finish.
…0891) - Enter Pipeline.__enter__ across constructor(pipeline) and pipeline.run() (closing _extra_context in finally) so cached Java expansion service subprocesses stay alive through artifact staging and --beam_services overrides apply to both steps. - Honor --no_wait_until_finish by skipping wait_until_finish() and terminal state checks when set. - Escape temp file paths with json.dumps in main_test.py so Windows backslash paths parse as valid YAML strings, and skip RunTest when jsonschema is unavailable (matching apache_beam/yaml/main_test.py).
damccorm
left a comment
There was a problem hiding this comment.
Thanks - I'll merge after checks complete.
We should also follow up with a documentation PR for the website (and maybe even a beam blog) after the 2.79 release in late December or early January
| tricky_input = os.path.join(self.tmp, r'C_\Users\tmp_users.csv') | ||
| shutil.copyfile(self.input, tricky_input) |
There was a problem hiding this comment.
This is causing the Windows unit test suites to fail with FileNotFoundError: on POSIX \ is a literal filename character, whereas on Windows \ is the directory separator so tricky_input resolves to <self.tmp>\C_\Users\tmp_users.csv whose parent directory C_\Users does not exist yet.
| tricky_input = os.path.join(self.tmp, r'C_\Users\tmp_users.csv') | |
| shutil.copyfile(self.input, tricky_input) | |
| tricky_input = os.path.join(self.tmp, r'C_\Users\tmp_users.csv') | |
| os.makedirs(os.path.dirname(tricky_input), exist_ok=True) | |
| shutil.copyfile(self.input, tricky_input) |
damccorm
left a comment
There was a problem hiding this comment.
Could you please fix the failing test before I merge?
Addresses #40891
beam run FILE runs a Beam YAML pipeline file like
python -m apache_beam.yaml.main --yaml_pipeline_file=FILE, passing every other flag through as a pipeline option. It reports errors without tracebacks (unless --verbose), uses documented exit codes, cancels the pipeline on Ctrl-C, and names the runner that ran the pipeline.