Skip to content

Commit ed3e5b1

Browse files
committed
Add the directory with staged files to sys.path and document the usage
of `--files_to_stage` and `--beam_plugins`
1 parent abe3fc7 commit ed3e5b1

8 files changed

Lines changed: 185 additions & 3 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"comment": "Modify this file in a trivial way to cause this test suite to run.",
33
"pr": "38701",
4-
"modification": 55
4+
"modification": 56
55
}

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070

7171
* (Python) Removed the `envoy-data-plane` (and transitive `betterproto`) dependency; `EnvoyRateLimiter` now uses a small vendored protobuf definition instead, resolving dependency conflicts for downstream projects ([#37854](https://github.com/apache/beam/issues/37854)).
7272
* (Java) Supported acknowledge mode for JmsIO ([#39253](https://github.com/apache/beam/issues/39253)).
73-
* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)).
73+
* (Python) Staged files directory is now automatically added to `sys.path` on the Python SDK worker at startup. This makes Python files provided via the '--files_to_stage' pipeline option importable in the pipeline code and makes it easier to initialize Python SDK harness at startup via the `--beam_plugins` pipeline option. For more information, see the [Staging Individual Files](https://beam.apache.org/documentation/sdks/python-pipeline-dependencies/#staging-files) section of the dependency management docs. This behavior can be disabled by passing the '--experiments=no_staged_dir_in_sys_path' pipeline option ([#39431](https://github.com/apache/beam/issues/39431)).
7474

7575
## Breaking Changes
7676

sdks/python/apache_beam/options/pipeline_options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,6 +1863,7 @@ def _add_argparse_args(cls, parser):
18631863
'workers will install them in same order they were specified on '
18641864
'the command line.'))
18651865
parser.add_argument(
1866+
'--file_to_stage',
18661867
'--files_to_stage',
18671868
dest='files_to_stage',
18681869
action='append',

sdks/python/apache_beam/options/pipeline_options_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,22 @@ def test_extra_package(self):
569569
options = PipelineOptions(flags=[''])
570570
self.assertEqual(options.get_all_options()['extra_packages'], None)
571571

572+
def test_files_to_stage(self):
573+
options = PipelineOptions([
574+
'--file_to_stage',
575+
'abc',
576+
'--files_to_stage',
577+
'def',
578+
'--files_to_stage',
579+
'ghi'
580+
])
581+
self.assertEqual(
582+
sorted(options.get_all_options()['files_to_stage']),
583+
['abc', 'def', 'ghi'])
584+
585+
options = PipelineOptions(flags=[''])
586+
self.assertEqual(options.get_all_options()['files_to_stage'], None)
587+
572588
def test_dataflow_job_file(self):
573589
options = PipelineOptions(['--dataflow_job_file', 'abc'])
574590
self.assertEqual(options.get_all_options()['dataflow_job_file'], 'abc')
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
import logging
19+
import os
20+
import shutil
21+
import tempfile
22+
import unittest
23+
import uuid
24+
25+
import pytest
26+
27+
import apache_beam as beam
28+
from apache_beam.options.pipeline_options import SetupOptions
29+
from apache_beam.testing.test_pipeline import TestPipeline
30+
from apache_beam.testing.util import assert_that
31+
from apache_beam.testing.util import equal_to
32+
33+
_LOGGER = logging.getLogger(__name__)
34+
35+
36+
class BeamPluginsIT(unittest.TestCase):
37+
def setUp(self):
38+
self.temp_dir = tempfile.mkdtemp()
39+
40+
def tearDown(self):
41+
shutil.rmtree(self.temp_dir)
42+
43+
@pytest.mark.it_postcommit
44+
def test_beam_plugins_staging(self):
45+
pipeline = TestPipeline(is_integration_test=True)
46+
setup_options = pipeline.options.view_as(SetupOptions)
47+
48+
plugin_name = f'beam_integration_plugin_{uuid.uuid4().hex[:8]}'
49+
plugin_file_path = os.path.join(self.temp_dir, f'{plugin_name}.py')
50+
51+
with open(plugin_file_path, 'w') as f:
52+
f.write("import sys\nsys.beam_plugin_loaded_for_test = True\n")
53+
54+
staged_files = setup_options.files_to_stage or []
55+
staged_files.append(plugin_file_path)
56+
setup_options.files_to_stage = staged_files
57+
setup_options.beam_plugins = [plugin_name]
58+
59+
def check_plugin_loaded(_):
60+
import sys
61+
return getattr(sys, 'beam_plugin_loaded_for_test', False)
62+
63+
with pipeline as p:
64+
res = (p | beam.Create([None]) | beam.Map(check_plugin_loaded))
65+
assert_that(res, equal_to([True]))
66+
67+
68+
if __name__ == '__main__':
69+
logging.getLogger().setLevel(logging.INFO)
70+
unittest.main()

sdks/python/apache_beam/runners/worker/sdk_worker_main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
_LOGGER = logging.getLogger(__name__)
5151
_ENABLE_GOOGLE_CLOUD_PROFILER = 'enable_google_cloud_profiler'
5252
_FN_LOG_HANDLER = None
53+
_STAGED_DIRECTORY = 'staged'
5354

5455

5556
def _import_beam_plugins(plugins):
@@ -131,6 +132,12 @@ def create_harness(environment, dry_run=False):
131132
environment.get('RUNNER_CAPABILITIES', '').split())
132133

133134
_LOGGER.info('semi_persistent_directory: %s', semi_persistent_directory)
135+
experiments = sdk_pipeline_options.view_as(DebugOptions).experiments or []
136+
if 'no_staged_dir_in_sys_path' not in experiments and semi_persistent_directory:
137+
staged_dir = os.path.join(semi_persistent_directory, _STAGED_DIRECTORY)
138+
if os.path.isdir(staged_dir) and staged_dir not in sys.path:
139+
sys.path.append(staged_dir)
140+
134141
_worker_id = environment.get('WORKER_ID', None)
135142

136143
try:

sdks/python/apache_beam/runners/worker/sdk_worker_main_test.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,55 @@ def __init__(self, *unused):
129129
def test_import_beam_plugins(self):
130130
sdk_worker_main._import_beam_plugins(BeamPlugin.get_all_plugin_paths())
131131

132+
def test_create_harness_adds_staged_dir_to_sys_path(self):
133+
import sys
134+
import tempfile
135+
136+
with tempfile.TemporaryDirectory() as temp_dir:
137+
staged_dir = os.path.join(temp_dir, sdk_worker_main._STAGED_DIRECTORY)
138+
os.mkdir(staged_dir)
139+
140+
env = {
141+
'CONTROL_API_SERVICE_DESCRIPTOR': '',
142+
'SEMI_PERSISTENT_DIRECTORY': temp_dir,
143+
}
144+
145+
if staged_dir in sys.path:
146+
sys.path.remove(staged_dir)
147+
148+
sdk_worker_main.create_harness(env, dry_run=True)
149+
150+
try:
151+
self.assertIn(staged_dir, sys.path)
152+
finally:
153+
if staged_dir in sys.path:
154+
sys.path.remove(staged_dir)
155+
156+
def test_create_harness_does_not_add_staged_dir_with_experiment(self):
157+
import sys
158+
import tempfile
159+
160+
with tempfile.TemporaryDirectory() as temp_dir:
161+
staged_dir = os.path.join(temp_dir, sdk_worker_main._STAGED_DIRECTORY)
162+
os.mkdir(staged_dir)
163+
164+
env = {
165+
'CONTROL_API_SERVICE_DESCRIPTOR': '',
166+
'SEMI_PERSISTENT_DIRECTORY': temp_dir,
167+
'PIPELINE_OPTIONS': '{"experiments":["no_staged_dir_in_sys_path"]}',
168+
}
169+
170+
if staged_dir in sys.path:
171+
sys.path.remove(staged_dir)
172+
173+
sdk_worker_main.create_harness(env, dry_run=True)
174+
175+
try:
176+
self.assertNotIn(staged_dir, sys.path)
177+
finally:
178+
if staged_dir in sys.path:
179+
sys.path.remove(staged_dir)
180+
132181
@staticmethod
133182
def _overrides_case_to_option_dict(case):
134183
"""

website/www/site/content/en/documentation/sdks/python-pipeline-dependencies.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,48 @@ If your pipeline uses packages that are not available publicly (e.g. packages th
9191

9292
See the [build documentation](https://pypa-build.readthedocs.io/en/latest/index.html) for more details on this command.
9393

94+
## Staging Individual Files {#staging-files}
95+
96+
If your pipeline relies on one or more individual Python files or non-Python data files that do not need to be packaged as a full Python package, you can stage them individually to the remote workers.
97+
98+
To stage individual files, run your pipeline with the `--files_to_stage` (or `--file_to_stage`) pipeline option. This option accepts a list of local file paths:
99+
100+
--files_to_stage="/path/to/my_module.py,/path/to/data_config.json"
101+
102+
When the pipeline runs, the runner uploads these files and makes them available on the workers in the worker's staged files directory.
103+
104+
### Accessing Staged Files on the Workers
105+
106+
Staged files are downloaded onto the worker:
107+
108+
* **Python modules**: Starting with Apache Beam 2.76.0, to make it easy to import staged Python files as modules, Beam automatically appends the worker's staged files directory to the Python interpreter's search path (`sys.path`) during startup. This means you can import them directly in your code:
109+
110+
import my_module
111+
112+
* **Data or configuration files**: If you staged non-Python files (such as a JSON config), they are downloaded to the staged files directory. You can locate this directory on the worker by reading the `SEMI_PERSISTENT_DIRECTORY` environment variable, or look for `/tmp/staged` which is the default location for staged files on containerized runners:
113+
114+
import os
115+
staged_dir = os.environ.get('SEMI_PERSISTENT_DIRECTORY', '/tmp/staged')
116+
config_path = os.path.join(staged_dir, 'data_config.json')
117+
118+
### Importing Plugins on Worker Startup
119+
120+
You can use staged files in combination with the `--beam_plugins` pipeline option (supported starting with Apache Beam 2.76.0) to run initialization code on the workers before any processing starts.
121+
122+
To use a staged file as a plugin:
123+
1. Stage the plugin file (e.g. `my_custom_plugin.py`) using `--file_to_stage`.
124+
2. Reference the module name in `--beam_plugins`.
125+
126+
For example, run your pipeline with:
127+
128+
--file_to_stage="/path/to/my_custom_plugin.py" \
129+
--beam_plugins="my_custom_plugin"
130+
131+
This instructs the SDK worker to import `my_custom_plugin` immediately on startup, triggering any initialization logic defined in the module.
132+
94133
## Multiple File Dependencies {#multiple-file-dependencies}
95134

96-
Often, your pipeline code spans multiple files. To run your project remotely, you must group these files as a Python package and specify the package when you run your pipeline. When the remote workers start, they will install your package. To group your files as a Python package and make it available remotely, perform the following steps:
135+
Often, your pipeline code spans multiple files. To run your project remotely, it is best to group these files as a Python package and specify the package when you run your pipeline. When the remote workers start, they will install your package. To group your files as a Python package and make it available remotely, perform the following steps:
97136

98137
1. Create a [setup.py](https://pythonhosted.org/an_example_pypi_project/setuptools.html) file for your project. The following is a very basic `setup.py` file.
99138

0 commit comments

Comments
 (0)